使用eBPF在内核空间构建DDoS攻击检测与自动防御系统
DDoS(分布式拒绝服务)攻击是常见的网络安全威胁,攻击者通过控制大量“肉鸡”向目标服务器发送海量请求,导致服务器资源耗尽,无法正常提供服务。传统的DDoS防御方案通常依赖于部署在网络边缘的硬件设备或云服务,但这些方案往往成本高昂,且存在一定的延迟。eBPF(扩展的伯克利包过滤器)作为一种强大的内核技术,允许开发者在内核空间安全高效地运行自定义代码,为DDoS防御提供了新的思路。
本文将介绍如何使用eBPF程序在Linux内核中实现一个简易的DDoS攻击检测器,并能够自动阻断恶意流量。该检测器通过监控网络流量,识别异常流量模式,并采取相应的防御措施。
1. eBPF简介
eBPF 是一种革命性的内核技术,它允许用户在内核中安全地运行自定义代码,而无需修改内核源代码或加载内核模块。eBPF程序通常由用户空间的应用程序加载到内核中,并在特定的事件发生时被触发,例如网络数据包到达或系统调用发生。
eBPF程序具有以下优点:
- 安全: eBPF程序在加载到内核之前会经过严格的验证,确保程序的安全性和稳定性。
- 高效: eBPF程序在内核空间运行,避免了用户空间和内核空间之间频繁的数据拷贝和上下文切换,提高了性能。
- 灵活: eBPF程序可以用于各种用途,例如网络监控、性能分析、安全审计等。
2. DDoS攻击检测原理
DDoS攻击通常具有以下特征:
- 高流量: 攻击者会发送大量的请求,导致网络流量异常增加。
- 源地址欺骗: 攻击者可能会伪造源IP地址,增加追踪难度。
- 请求模式异常: 攻击者可能会发送特定类型的请求,例如SYN flood或UDP flood。
基于以上特征,我们可以通过以下方式检测DDoS攻击:
- 流量监控: 监控网络流量,检测流量是否超过预设的阈值。
- 源IP地址分析: 分析源IP地址的分布,检测是否存在大量的来自同一IP地址的请求。
- 请求类型分析: 分析请求的类型,检测是否存在特定类型的攻击请求。
3. eBPF程序设计
我们的eBPF程序将实现以下功能:
- 流量监控: 统计每个源IP地址的请求数量。
- 异常检测: 检测源IP地址的请求数量是否超过预设的阈值。
- 流量阻断: 阻断来自异常IP地址的流量。
3.1 数据结构定义
我们需要定义以下数据结构:
ip_counts: 用于存储每个源IP地址的请求数量,使用eBPF的map数据结构实现。blocked_ips: 用于存储被阻断的IP地址,也使用eBPF的map数据结构实现。
// 定义IP地址计数map
BPF_MAP_DEF(ip_counts, LRU_HASH, __u32, __u64, 65536, 0);
BPF_MAP_ADD(ip_counts);
// 定义被阻止IP地址的map
BPF_MAP_DEF(blocked_ips, HASH, __u32, __u8, 65536, 0);
BPF_MAP_ADD(blocked_ips);
// 定义配置结构体
struct config {
__u64 threshold; // 流量阈值
};
BPF_MAP_DEF(config_map, ARRAY, __u32, struct config, 1, 0);
BPF_MAP_ADD(config_map);
3.2 eBPF程序代码
eBPF程序将挂载到tc(Traffic Control)的ingress(入站) hook点,对每个进入的网络数据包进行处理。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#define MAX_TCP_OPTIONS_SIZE 40
// 定义IP地址计数map
BPF_MAP_DEF(ip_counts, LRU_HASH, __u32, __u64, 65536, 0);
BPF_MAP_ADD(ip_counts);
// 定义被阻止IP地址的map
BPF_MAP_DEF(blocked_ips, HASH, __u32, __u8, 65536, 0);
BPF_MAP_ADD(blocked_ips);
// 定义配置结构体
struct config {
__u64 threshold; // 流量阈值
};
BPF_MAP_DEF(config_map, ARRAY, __u32, struct config, 1, 0);
BPF_MAP_ADD(config_map);
SEC("ingress")
int handle_ingress(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// 以太网头部
struct ethhdr *eth = data;
if (data + sizeof(struct ethhdr) > data_end)
return TC_ACT_OK;
// 只处理IP数据包
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
// IP头部
struct iphdr *iph = data + sizeof(struct ethhdr);
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)
return TC_ACT_OK;
__u32 src_ip = iph->saddr;
// 获取配置
__u32 key = 0;
struct config *cfg = bpf_map_lookup_elem(&config_map, &key);
if (!cfg) {
// 默认阈值
return TC_ACT_OK;
}
// 检查IP是否被阻止
if (bpf_map_lookup_elem(&blocked_ips, &src_ip)) {
// 如果IP被阻止,则丢弃数据包
return TC_ACT_DROP;
}
// 增加IP地址的计数
__u64 *count = bpf_map_lookup_elem(&ip_counts, &src_ip);
if (count) {
(*count)++;
} else {
__u64 init_count = 1;
bpf_map_update_elem(&ip_counts, &src_ip, &init_count, BPF_ANY);
}
// 检查是否超过阈值
count = bpf_map_lookup_elem(&ip_counts, &src_ip);
if (count && *count > cfg->threshold) {
// 超过阈值,阻止该IP地址
__u8 blocked = 1;
bpf_map_update_elem(&blocked_ips, &src_ip, &blocked, BPF_ANY);
bpf_printk("DDoS detected, blocking IP: %x\n", src_ip);
return TC_ACT_DROP; // 丢弃数据包
}
return TC_ACT_OK; // 允许数据包通过
}
char _license[] SEC("license") = "GPL";
3.3 代码解释
- 包含头文件: 包含必要的头文件,例如
linux/bpf.h、bpf/bpf_helpers.h等。 - 定义Map: 定义
ip_counts和blocked_ips两个Map,用于存储IP地址的计数和被阻止的IP地址。 handle_ingress函数: 这是eBPF程序的入口函数,当网络数据包到达ingresshook点时,该函数会被调用。- 数据包解析: 解析以太网头部和IP头部,获取源IP地址。
- 检查是否被阻止: 检查源IP地址是否在
blocked_ipsMap中,如果在,则丢弃该数据包。 - 增加计数: 增加源IP地址的计数,如果该IP地址是第一次出现,则在
ip_countsMap中创建一个新的条目。 - 检查阈值: 检查源IP地址的计数是否超过预设的阈值,如果超过,则将该IP地址添加到
blocked_ipsMap中,并丢弃该数据包。 - 返回: 如果数据包没有被阻止,则返回
TC_ACT_OK,允许数据包通过。
4. 用户空间程序
用户空间程序负责将eBPF程序加载到内核,并设置阈值。
4.1 加载eBPF程序
使用libbpf库加载eBPF程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "ddos_detect.skel.h" // 由 bpftool gen skeleton 生成
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
static void sig_int(int signo)
{
exit(0);
}
int main(int argc, char **argv)
{
struct rlimit rlim = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY,
};
if (setrlimit(RLIMIT_MEMLOCK, &rlim)) {
fprintf(stderr, "failed to increase RLIMIT_MEMLOCK limit!\n");
exit(1);
}
libbpf_set_print(libbpf_print_fn);
struct ddos_detect_bpf *skel = ddos_detect_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 加载和验证BPF程序
int err = ddos_detect_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
// 设置流量阈值
__u32 key = 0;
struct config cfg = {.threshold = 1000}; // 设置阈值为1000
err = bpf_map_update_elem(bpf_map__fd(skel->maps.config_map), &key, &cfg, BPF_ANY);
if (err) {
fprintf(stderr, "Failed to set threshold: %s\n", strerror(errno));
goto cleanup;
}
// 附加BPF程序
err = ddos_detect_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("DDoS detect eBPF program loaded and running...\n");
signal(SIGINT, sig_int);
while (1) {
sleep(1);
}
cleanup:
ddos_detect_bpf__destroy(skel);
return -err;
}
4.2 代码解释
- 包含头文件: 包含必要的头文件,例如
stdio.h、stdlib.h、bpf/libbpf.h等。 ddos_detect.skel.h: 这个头文件是由bpftool gen skeleton命令生成的,它包含了eBPF程序的结构体定义和相关的函数声明。- 加载eBPF程序: 使用
ddos_detect_bpf__open函数打开eBPF程序,使用ddos_detect_bpf__load函数加载和验证eBPF程序,使用ddos_detect_bpf__attach函数将eBPF程序附加到tc的ingresshook点。 - 设置阈值: 设置流量阈值,例如设置为1000,表示当来自同一个IP地址的请求数量超过1000时,就认为该IP地址正在进行DDoS攻击。
- 循环: 程序进入一个无限循环,保持eBPF程序运行。
5. 编译和运行
5.1 编译eBPF程序
使用clang编译eBPF程序,并生成eBPF目标文件。
clang -target bpf -D__KERNEL__ -I./include -I./usr/include -c ddos_detect.c -o ddos_detect.o
5.2 生成BPF骨架代码
使用bpftool生成BPF骨架代码,用于简化用户空间程序的开发。
bpftool gen skeleton ddos_detect.o -o ddos_detect.skel.h
5.3 编译用户空间程序
使用gcc编译用户空间程序。
gcc -Wall ddos_detect_user.c -o ddos_detect_user -lbpf -lz
5.4 运行程序
配置tc: 首先需要配置
tc,将ingresshook点添加到网络接口。# 替换 eth0 为你的网卡名称 sudo tc qdisc add dev eth0 clsact sudo tc filter add dev eth0 ingress bpf da obj ddos_detect.o sec ingress运行用户空间程序:
sudo ./ddos_detect_user
6. 测试
可以使用hping3等工具模拟DDoS攻击,测试eBPF程序的防御效果。
sudo hping3 -c 2000 -d 120 -S -p 80 --flood --rand-source <目标IP地址>
7. 总结
本文介绍了如何使用eBPF技术在Linux内核中构建一个简易的DDoS攻击检测器,并实现自动阻断恶意流量的功能。该方案具有以下优点:
- 高效: eBPF程序在内核空间运行,避免了用户空间和内核空间之间频繁的数据拷贝和上下文切换,提高了性能。
- 实时: eBPF程序可以实时监控网络流量,及时发现和阻断DDoS攻击。
- 灵活: 可以根据实际需求,自定义eBPF程序,实现更复杂的DDoS防御策略。
当然,本文介绍的只是一个简易的DDoS攻击检测器,实际的DDoS攻击防御需要更复杂的策略和技术,例如:
- 更精确的流量分析: 使用机器学习等技术,对流量进行更深入的分析,识别更复杂的攻击模式。
- 动态阈值调整: 根据网络流量的变化,动态调整阈值,避免误判。
- 多层防御: 结合其他防御手段,例如CDN、Web应用防火墙等,构建多层防御体系。
希望本文能够帮助读者理解和掌握如何利用eBPF技术进行DDoS防御,为构建更安全、更可靠的网络环境提供参考。