WEBKT

eBPF流量整形实战-如何用eBPF限制特定IP/端口的带宽?

71 0 0 0

eBPF流量整形实战-如何用eBPF限制特定IP/端口的带宽?

1. 为什么选择eBPF?

2. 流量整形的基本原理

3. eBPF流量整形器的设计

3.1 数据结构

3.2 eBPF程序

3.3 用户态程序

4. 编译和运行

4.1 编译eBPF程序

4.2 编译用户态程序

4.3 运行程序

5. 验证流量整形效果

6. 总结与展望

eBPF流量整形实战-如何用eBPF限制特定IP/端口的带宽?

作为一名网络工程师,你是否经常遇到这样的问题:某些用户或服务占用了过多的带宽,导致其他用户的网络体验变差?传统的流量整形方案往往配置复杂,性能损耗大。今天,我将带你使用eBPF来实现一个简单而高效的流量整形器,它可以限制特定IP地址或端口的带宽使用,有效防止恶意用户占用过多资源。

1. 为什么选择eBPF?

在深入代码之前,我们先来了解一下为什么eBPF是流量整形的一个好选择。

  • 高性能: eBPF程序运行在内核态,避免了用户态和内核态之间频繁的上下文切换,从而提高了性能。
  • 灵活性: eBPF程序可以动态加载和卸载,无需修改内核代码,方便部署和维护。
  • 安全性: eBPF程序在运行前会经过内核的验证,确保程序的安全性,防止恶意代码破坏系统。

2. 流量整形的基本原理

流量整形,顾名思义,就是对网络流量进行“整形”,使其符合一定的规则。常见的流量整形算法包括:

  • 令牌桶(Token Bucket): 令牌桶算法是一种常用的流量整形算法。它维护一个令牌桶,以一定的速率向桶中添加令牌。每个数据包需要消耗一个令牌才能通过。如果桶中没有足够的令牌,数据包将被延迟或丢弃。
  • 漏桶(Leaky Bucket): 漏桶算法以恒定的速率从桶中“漏”出数据包。如果数据包到达的速度超过了漏出的速度,数据包将被放入桶中等待。如果桶满了,数据包将被丢弃。

在本文中,我们将使用令牌桶算法来实现流量整形器。

3. eBPF流量整形器的设计

我们的eBPF流量整形器将实现以下功能:

  • 基于IP地址和端口的流量限制: 可以针对特定的IP地址或端口设置带宽限制。
  • 动态配置: 可以动态修改带宽限制,无需重启系统。
  • 可观测性: 可以监控流量整形器的运行状态,例如丢包率、延迟等。

3.1 数据结构

为了实现上述功能,我们需要定义以下数据结构:

// 定义一个结构体,用于存储IP地址和端口信息
struct ip_port_key_t {
__u32 ip;
__u16 port;
};
// 定义一个结构体,用于存储令牌桶信息
struct token_bucket_t {
__u64 tokens; // 当前令牌数
__u64 last_time; // 上次更新令牌的时间
__u64 rate; // 令牌生成速率
__u64 burst; // 令牌桶容量
};
// 定义一个BPF哈希表,用于存储IP地址/端口和令牌桶的映射关系
BPF_TABLE("hash", struct ip_port_key_t, struct token_bucket_t, ip_port_map, 65536);
  • ip_port_key_t:用于存储IP地址和端口信息,作为BPF哈希表的键。
  • token_bucket_t:用于存储令牌桶信息,包括当前令牌数、上次更新令牌的时间、令牌生成速率和令牌桶容量,作为BPF哈希表的值。
  • ip_port_map:BPF哈希表,用于存储IP地址/端口和令牌桶的映射关系。哈希表的大小设置为65536,可以存储大量的IP地址/端口信息。

3.2 eBPF程序

eBPF程序的主要流程如下:

  1. 获取数据包的源IP地址和端口。
  2. 在BPF哈希表中查找对应的令牌桶。
  3. 如果找到了令牌桶,则更新令牌数。
  4. 如果令牌数足够,则允许数据包通过,并消耗一个令牌。
  5. 如果令牌数不足,则丢弃数据包。

下面是eBPF程序的代码:

#include <uapi/linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 定义一个结构体,用于存储IP地址和端口信息
struct ip_port_key_t {
__u32 ip;
__u16 port;
};
// 定义一个结构体,用于存储令牌桶信息
struct token_bucket_t {
__u64 tokens; // 当前令牌数
__u64 last_time; // 上次更新令牌的时间
__u64 rate; // 令牌生成速率
__u64 burst; // 令牌桶容量
};
// 定义一个BPF哈希表,用于存储IP地址/端口和令牌桶的映射关系
BPF_TABLE("hash", struct ip_port_key_t, struct token_bucket_t, ip_port_map, 65536);
int packet_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// 解析IP头部
struct iphdr *iph = data;
if (iph + 1 > data_end)
return XDP_PASS;
if (iph->version != 4 || iph->ihl < 5)
return XDP_PASS;
// 只处理TCP协议
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
// 解析TCP头部
struct tcphdr *tcph = (void *)iph + iph->ihl * 4;
if (tcph + 1 > data_end)
return XDP_PASS;
// 获取源IP地址和端口
struct ip_port_key_t key = {
.ip = bpf_ntohl(iph->saddr),
.port = bpf_ntohs(tcph->source)
};
// 在BPF哈希表中查找对应的令牌桶
struct token_bucket_t *bucket = ip_port_map.lookup(&key);
if (!bucket)
return XDP_PASS;
// 获取当前时间
__u64 now = bpf_ktime_get_ns();
// 计算自上次更新令牌以来经过的时间
__u64 delta = now - bucket->last_time;
// 更新令牌数
bucket->tokens += delta * bucket->rate / 1000000000; // 纳秒转换为秒
if (bucket->tokens > bucket->burst)
bucket->tokens = bucket->burst;
// 更新上次更新令牌的时间
bucket->last_time = now;
// 如果令牌数足够,则允许数据包通过,并消耗一个令牌
if (bucket->tokens >= (__u64)bpf_ntohs(iph->tot_len)) {
bucket->tokens -= (__u64)bpf_ntohs(iph->tot_len);
ip_port_map.update(&key, bucket);
return XDP_PASS;
}
// 如果令牌数不足,则丢弃数据包
return XDP_DROP;
}

代码解释:

  • 首先,我们包含了必要的头文件,包括bpf.hin.hip.htcp.h。这些头文件定义了eBPF程序所需的各种数据结构和函数。
  • packet_filter函数是eBPF程序的入口点。它接收一个xdp_md结构体作为参数,该结构体包含了数据包的信息。
  • 在函数内部,我们首先解析IP头部和TCP头部,获取源IP地址和端口。注意,我们需要使用bpf_ntohlbpf_ntohs函数将网络字节序转换为本机字节序。
  • 然后,我们在BPF哈希表中查找对应的令牌桶。如果找到了令牌桶,则更新令牌数。更新令牌数的公式为:tokens = tokens + delta * rate,其中delta为自上次更新令牌以来经过的时间,rate为令牌生成速率。为了防止令牌数超过令牌桶的容量,我们需要将令牌数限制在burst值之内。
  • 最后,我们判断令牌数是否足够。如果令牌数大于数据包的长度,则允许数据包通过,并消耗一个令牌。否则,丢弃数据包。

3.3 用户态程序

用户态程序的主要功能如下:

  • 加载eBPF程序到内核。
  • 创建和管理BPF哈希表。
  • 提供API,用于动态配置带宽限制。
  • 监控流量整形器的运行状态。

下面是用户态程序的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
// 定义一个结构体,用于存储IP地址和端口信息
struct ip_port_key_t {
__u32 ip;
__u16 port;
};
// 定义一个结构体,用于存储令牌桶信息
struct token_bucket_t {
__u64 tokens; // 当前令牌数
__u64 last_time; // 上次更新令牌的时间
__u64 rate; // 令牌生成速率
__u64 burst; // 令牌桶容量
};
int main(int argc, char **argv) {
// 加载eBPF程序
struct bpf_object *obj = bpf_object__open("packet_filter.o");
if (!obj) {
fprintf(stderr, "Failed to open BPF object\n");
return 1;
}
int err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "Failed to load BPF object\n");
return 1;
}
// 获取BPF哈希表的fd
int map_fd = bpf_object__find_map_fd_by_name(obj, "ip_port_map");
if (map_fd < 0) {
fprintf(stderr, "Failed to find map\n");
return 1;
}
// 设置IP地址和端口的带宽限制
struct ip_port_key_t key = {
.ip = inet_addr("192.168.1.100"),
.port = htons(8080)
};
struct token_bucket_t bucket = {
.tokens = 1000000, // 初始令牌数
.last_time = bpf_ktime_get_ns(), // 上次更新令牌的时间
.rate = 1000000, // 令牌生成速率,单位:字节/秒
.burst = 10000000 // 令牌桶容量,单位:字节
};
err = bpf_map_update_elem(map_fd, &key, &bucket, BPF_ANY);
if (err) {
fprintf(stderr, "Failed to update map: %s\n", strerror(errno));
return 1;
}
printf("Traffic shaping configured for 192.168.1.100:8080\n");
// 等待一段时间
sleep(60);
// 卸载eBPF程序
bpf_object__close(obj);
return 0;
}

代码解释:

  • 首先,我们使用bpf_object__open函数打开eBPF程序的目标文件(packet_filter.o)。
  • 然后,使用bpf_object__load函数将eBPF程序加载到内核。
  • 接下来,使用bpf_object__find_map_fd_by_name函数获取BPF哈希表的fd。map_fd是后续操作BPF哈希表的关键。
  • 然后,我们创建一个ip_port_key_t结构体,用于指定要限制带宽的IP地址和端口。注意,我们需要使用inet_addrhtons函数将IP地址和端口转换为网络字节序。
  • 接下来,我们创建一个token_bucket_t结构体,用于设置令牌桶的参数,包括初始令牌数、上次更新令牌的时间、令牌生成速率和令牌桶容量。
  • 最后,我们使用bpf_map_update_elem函数将IP地址/端口和令牌桶的映射关系添加到BPF哈希表中。BPF_ANY标志表示如果该键不存在,则创建一个新的键值对;如果该键已经存在,则更新该键对应的值。

4. 编译和运行

4.1 编译eBPF程序

首先,我们需要安装必要的工具链,包括clangllvm。然后,使用以下命令编译eBPF程序:

clang -O2 -target bpf -c packet_filter.c -o packet_filter.o

4.2 编译用户态程序

接下来,我们需要安装libbpf库。然后,使用以下命令编译用户态程序:

gcc -Wall user_program.c -o user_program -lbpf

4.3 运行程序

首先,我们需要将eBPF程序加载到XDP(eXpress Data Path)。XDP是一种高性能的网络数据包处理框架,它可以将eBPF程序附加到网络接口上,从而实现对数据包的快速处理。可以使用ip link命令将eBPF程序附加到网络接口上:

ip link set dev eth0 xdp obj packet_filter.o

其中,eth0为网络接口的名称,packet_filter.o为eBPF程序的目标文件。

然后,我们可以运行用户态程序:

sudo ./user_program

运行结果如下:

Traffic shaping configured for 192.168.1.100:8080

这表示我们已经成功地为IP地址192.168.1.100的端口8080设置了带宽限制。

5. 验证流量整形效果

为了验证流量整形效果,我们可以使用iperf3工具来测试带宽。首先,在一台机器上运行iperf3服务器:

iperf3 -s

然后,在另一台机器上运行iperf3客户端,连接到服务器的8080端口:

iperf3 -c 192.168.1.100 -p 8080

如果流量整形器工作正常,iperf3客户端的带宽应该被限制在我们设置的速率之内。

6. 总结与展望

本文介绍了如何使用eBPF来实现一个简单的流量整形器,它可以限制特定IP地址或端口的带宽使用。通过使用eBPF,我们可以实现高性能、灵活和安全的流量整形方案。

当然,本文介绍的流量整形器还比较简单,只实现了基本的流量限制功能。在实际应用中,我们还可以添加更多的功能,例如:

  • 支持更多的流量整形算法: 除了令牌桶算法,我们还可以支持漏桶算法、PQ算法等。
  • 支持更细粒度的流量控制: 可以根据不同的服务类型、用户优先级等进行流量控制。
  • 支持动态调整带宽限制: 可以根据网络状况动态调整带宽限制。
  • 提供更丰富的监控指标: 可以监控丢包率、延迟、队列长度等指标。

希望本文能够帮助你了解eBPF在流量整形方面的应用,并启发你使用eBPF来解决实际问题。

NetBoy eBPF流量整形网络限速

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9696