WEBKT

使用eBPF在内核空间构建DDoS攻击检测与自动防御系统

176 0 0 0

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 代码解释

  1. 包含头文件: 包含必要的头文件,例如linux/bpf.hbpf/bpf_helpers.h等。
  2. 定义Map: 定义ip_countsblocked_ips两个Map,用于存储IP地址的计数和被阻止的IP地址。
  3. handle_ingress函数: 这是eBPF程序的入口函数,当网络数据包到达ingress hook点时,该函数会被调用。
  4. 数据包解析: 解析以太网头部和IP头部,获取源IP地址。
  5. 检查是否被阻止: 检查源IP地址是否在blocked_ips Map中,如果在,则丢弃该数据包。
  6. 增加计数: 增加源IP地址的计数,如果该IP地址是第一次出现,则在ip_counts Map中创建一个新的条目。
  7. 检查阈值: 检查源IP地址的计数是否超过预设的阈值,如果超过,则将该IP地址添加到blocked_ips Map中,并丢弃该数据包。
  8. 返回: 如果数据包没有被阻止,则返回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 代码解释

  1. 包含头文件: 包含必要的头文件,例如stdio.hstdlib.hbpf/libbpf.h等。
  2. ddos_detect.skel.h 这个头文件是由bpftool gen skeleton命令生成的,它包含了eBPF程序的结构体定义和相关的函数声明。
  3. 加载eBPF程序: 使用ddos_detect_bpf__open函数打开eBPF程序,使用ddos_detect_bpf__load函数加载和验证eBPF程序,使用ddos_detect_bpf__attach函数将eBPF程序附加到tcingress hook点。
  4. 设置阈值: 设置流量阈值,例如设置为1000,表示当来自同一个IP地址的请求数量超过1000时,就认为该IP地址正在进行DDoS攻击。
  5. 循环: 程序进入一个无限循环,保持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 运行程序

  1. 配置tc: 首先需要配置tc,将ingress hook点添加到网络接口。

    # 替换 eth0 为你的网卡名称
    sudo tc qdisc add dev eth0 clsact
    sudo tc filter add dev eth0 ingress bpf da obj ddos_detect.o sec ingress
    
  2. 运行用户空间程序:

    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防御,为构建更安全、更可靠的网络环境提供参考。

安全小黑哥 eBPFDDoS防御网络安全

评论点评