XDP 编程:从加载、测试到应用
1. 验证 XDP 程序加载效果
在加载 XDP 程序后,需要验证其是否按预期工作。可以通过在外部机器上再次执行
nmap
命令来观察端口 8000 是否不再可达:
# nmap -sS 192.168.33.11
Starting Nmap 7.70 ( https://nmap.org ) at 2019-04-07 01:07 CEST
Nmap scan report for 192.168.33.11
Host is up (0.00039s latency).
Not shown: 998 closed ports
PORT STATE SERVICE
22/tcp open ssh
另外,还可以尝试通过浏览器访问该程序或进行任何 HTTP 请求。当以
192.168.33.11
为目标时,任何类型的测试都应该失败。
如果需要将机器恢复到原始状态,可以分离程序并关闭设备的 XDP:
# ip link set dev enp0s8 xdp off
2. 使用 BCC 加载 XDP 程序
2.1 内核空间程序
program.c
首先创建一个内核空间程序
program.c
,除了导入 BPF 和协议相关的结构体和函数定义所需的头文件外,还使用
BPF_TABLE
宏声明一个
BPF_MAP_TYPE_PERCPU_ARRAY
类型的映射,用于存储每个 IP 协议索引的数据包计数器:
#define KBUILD_MODNAME "program"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
BPF_TABLE("percpu_array", uint32_t, long, packetcnt, 256);
int myprogram(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
long *cnt;
__u32 idx;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_DROP;
}
idx = ip->protocol;
cnt = packetcnt.lookup(&idx);
if (cnt) {
*cnt += 1;
}
if (ip->protocol == IPPROTO_TCP) {
return XDP_DROP;
}
return XDP_PASS;
}
2.2 用户空间加载器
loader.py
加载器
loader.py
由实际的加载逻辑和打印数据包计数的循环两部分组成:
#!/usr/bin/python
from bcc import BPF
import time
import sys
device = "enp0s8"
b = BPF(src_file="program.c")
fn = b.load_func("myprogram", BPF.XDP)
b.attach_xdp(device, fn, 0)
packetcnt = b.get_table("packetcnt")
prev = [0] * 256
print("Printing packet counts per IP protocol-number, hit CTRL+C to stop")
while 1:
try:
for k in packetcnt.keys():
val = packetcnt.sum(k).value
i = k.value
if val:
delta = val - prev[i]
prev[i] = val
print("{}: {} pkt/s".format(i, delta))
time.sleep(1)
except KeyboardInterrupt:
print("Removing filter from device")
break
b.remove_xdp(device, 0)
使用 root 权限执行加载器来测试程序:
# python program.py
输出结果会每秒显示一行数据包计数器:
Printing packet counts per IP protocol-number, hit CTRL+C to stop
6: 10 pkt/s
17: 3 pkt/s
^CRemoving filter from device
3. 测试 XDP 程序
3.1 测试环境的挑战
在测试 XDP 程序时,最大的困难在于需要重现一个所有组件都能提供正确数据包的环境。虽然虚拟化技术使创建工作环境变得容易,但复杂的设置可能会限制测试环境的可重复性和可编程性。此外,在虚拟化环境中分析高频 XDP 程序的性能时,虚拟化成本会使测试变得无效。
3.2
BPF_PROG_TEST_RUN
命令
内核开发者实现了
BPF_PROG_TEST_RUN
命令来测试 XDP 程序。该命令可以执行 XDP 程序,并传入输入数据包和输出数据包。程序执行后,输出数据包变量会被填充,同时返回 XDP 代码。
3.3 使用 Python 单元测试框架进行 XDP 测试
以下是使用 Python 单元测试框架进行 XDP 测试的代码:
from bcc import BPF, libbcc
from scapy.all import Ether, IP, raw, TCP, UDP
import ctypes
import unittest
class XDPExampleTestCase(unittest.TestCase):
SKB_OUT_SIZE = 1514 # mtu 1500 + 14 ethernet size
bpf_function = None
def _xdp_test_run(self, given_packet, expected_packet, expected_return):
size = len(given_packet)
given_packet = ctypes.create_string_buffer(raw(given_packet), size)
packet_output = ctypes.create_string_buffer(self.SKB_OUT_SIZE)
packet_output_size = ctypes.c_uint32()
test_retval = ctypes.c_uint32()
duration = ctypes.c_uint32()
repeat = 1
ret = libbcc.lib.bpf_prog_test_run(self.bpf_function.fd,
repeat,
ctypes.byref(given_packet),
size,
ctypes.byref(packet_output),
ctypes.byref(packet_output_size),
ctypes.byref(test_retval),
ctypes.byref(duration))
self.assertEqual(ret, 0)
self.assertEqual(test_retval.value, expected_return)
if expected_packet:
self.assertEqual(
packet_output[:packet_output_size.value], raw(expected_packet))
def setUp(self):
bpf_prog = BPF(src_file=b"program.c")
self.bpf_function = bpf_prog.load_func(b"myprogram", BPF.XDP)
def test_drop_tcp(self):
given_packet = Ether() / IP() / TCP()
self._xdp_test_run(given_packet, None, BPF.XDP_DROP)
def test_pass_udp(self):
given_packet = Ether() / IP() / UDP()
expected_packet = Ether() / IP() / UDP()
self._xdp_test_run(given_packet, expected_packet, BPF.XDP_PASS)
def test_transform_dst(self):
given_packet = Ether() / IP() / TCP(dport=9090)
expected_packet = Ether(dst='08:00:27:dd:38:2a') / \
IP() / TCP(dport=9090)
self._xdp_test_run(given_packet, expected_packet, BPF.XDP_TX)
if __name__ == '__main__':
unittest.main()
3.4 测试用例说明
-
test_drop_tcp:测试是否会丢弃所有 TCP 数据包。 -
test_pass_udp:测试是否允许所有 UDP 数据包通过,并且数据包不会被修改。 -
test_transform_dst:测试当 TCP 数据包的目标端口为 9090 时,是否会更改其目标 MAC 地址并返回XDP_TX。
3.5 运行测试
使用以下命令运行测试:
sudo python test_xdp.py
输出结果会报告测试是否通过:
...
--------------------------------
Ran 3 tests in 4.676s
OK
如果修改
program.c
中的代码,例如将最后一个
XDP_PASS
改为
XDP_DROP
,再次运行测试,测试将会失败:
.F.
======================================================================
FAIL: test_pass_udp (__main__.XDPExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_xdp.py", line 48, in test_pass_udp
self._xdp_test_run(given_packet, expected_packet, BPF.XDP_PASS)
File "test_xdp.py", line 31, in _xdp_test_run
self.assertEqual(test_retval.value, expected_return)
AssertionError: 1 != 2
----------------------------------------------------------------------
Ran 3 tests in 4.667s
FAILED (failures=1)
4. XDP 的应用场景
4.1 网络监控
传统的网络监控系统要么通过编写内核模块,要么从用户空间访问
proc
文件来实现。但这些方法存在维护和调试困难、计算成本高等问题。而 XDP 可以使用 XDP 程序将需要提取的数据发送到映射中,然后由加载器将指标存储到存储后端,并应用算法或绘制结果图。
4.2 DDoS 缓解
在 NIC 级别就能看到数据包,确保在系统尚未花费足够的计算资源来判断数据包是否有用之前,就拦截任何可能的数据包。在典型场景中,
bpf
映射可以指示 XDP 程序丢弃来自特定源的数据包,从而使攻击者难以浪费系统的计算资源。
4.3 负载均衡
XDP 程序可以用于负载均衡,但 XDP 只能在数据包到达的同一 NIC 上重新传输数据包。
以下是 XDP 应用场景的总结表格:
| 应用场景 | 优势 |
| ---- | ---- |
| 网络监控 | 避免内核模块的维护和调试困难,减少计算成本 |
| DDoS 缓解 | 在早期拦截数据包,节省计算资源 |
| 负载均衡 | 可在 NIC 级别进行数据包处理 |
通过以上内容,我们了解了 XDP 程序的加载、测试和应用。希望这些信息能帮助你更好地使用 XDP 进行网络编程。
5. 网络监控的具体实现流程
5.1 编写 XDP 程序
首先,我们需要编写一个 XDP 程序来收集网络数据包的相关信息。以下是一个简单的示例:
#define KBUILD_MODNAME "monitor"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
BPF_TABLE("percpu_array", uint32_t, long, packet_cnt, 256);
int monitor(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
long *cnt;
__u32 idx;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_PASS;
}
idx = ip->protocol;
cnt = packet_cnt.lookup(&idx);
if (cnt) {
*cnt += 1;
}
return XDP_PASS;
}
这个程序会统计不同 IP 协议的数据包数量,并将结果存储在
packet_cnt
映射中。
5.2 编写用户空间加载器
接下来,我们需要编写一个用户空间加载器来加载 XDP 程序,并定期读取
packet_cnt
映射中的数据。以下是一个示例:
#!/usr/bin/python
from bcc import BPF
import time
device = "enp0s8"
b = BPF(src_file="monitor.c")
fn = b.load_func("monitor", BPF.XDP)
b.attach_xdp(device, fn, 0)
packet_cnt = b.get_table("packet_cnt")
prev = [0] * 256
print("Printing packet counts per IP protocol-number, hit CTRL+C to stop")
while 1:
try:
for k in packet_cnt.keys():
val = packet_cnt.sum(k).value
i = k.value
if val:
delta = val - prev[i]
prev[i] = val
print("{}: {} pkt/s".format(i, delta))
time.sleep(1)
except KeyboardInterrupt:
print("Removing filter from device")
break
b.remove_xdp(device, 0)
5.3 运行监控程序
使用 root 权限运行加载器:
# python monitor_loader.py
这样就可以实时监控不同 IP 协议的数据包流量。
5.4 监控流程的 mermaid 流程图
graph LR
A[开始] --> B[编写 XDP 程序]
B --> C[编写用户空间加载器];
C --> D[使用 root 权限运行加载器];
D --> E[实时监控数据包流量];
E --> F{是否停止};
F -- 否 --> E;
F -- 是 --> G[移除 XDP 程序];
G --> H[结束];
6. DDoS 缓解的详细实现
6.1 构建黑名单映射
在用户空间,我们需要构建一个黑名单映射,用于存储需要拦截的源 IP 地址。以下是一个简单的示例:
from bcc import BPF
import ctypes
device = "enp0s8"
b = BPF(src_file="ddos_mitigation.c")
fn = b.load_func("ddos_mitigation", BPF.XDP)
b.attach_xdp(device, fn, 0)
blacklist = b.get_table("blacklist")
# 添加需要拦截的源 IP 地址
ip = ctypes.c_uint32(0xc0a82101) # 192.168.33.1
blacklist[ip] = ctypes.c_uint32(1)
6.2 编写 XDP 程序
以下是一个根据黑名单拦截数据包的 XDP 程序:
#define KBUILD_MODNAME "ddos_mitigation"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
BPF_TABLE("hash", uint32_t, uint32_t, blacklist, 1024);
int ddos_mitigation(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_PASS;
}
uint32_t src_ip = ip->saddr;
if (blacklist.lookup(&src_ip)) {
return XDP_DROP;
}
return XDP_PASS;
}
6.3 DDoS 缓解流程的 mermaid 流程图
graph LR
A[开始] --> B[构建黑名单映射];
B --> C[编写 XDP 程序];
C --> D[加载 XDP 程序到网卡];
D --> E[数据包到达网卡];
E --> F{源 IP 是否在黑名单中};
F -- 是 --> G[丢弃数据包];
F -- 否 --> H[放行数据包];
G --> I[结束];
H --> I;
7. 负载均衡的实现思路
7.1 负载均衡的基本原理
XDP 负载均衡的基本原理是根据一定的规则将数据包分发到不同的后端服务器。由于 XDP 只能在同一 NIC 上重新传输数据包,我们可以通过修改数据包的目标 IP 地址来实现负载均衡。
7.2 编写 XDP 程序
以下是一个简单的负载均衡 XDP 程序示例:
#define KBUILD_MODNAME "load_balancer"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
BPF_TABLE("array", uint32_t, uint32_t, backend_ips, 2);
int load_balancer(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_PASS;
}
// 简单的轮询算法
static uint32_t index = 0;
uint32_t backend_ip = backend_ips.lookup(&index)->value;
ip->daddr = backend_ip;
index = (index + 1) % 2;
return XDP_TX;
}
7.3 负载均衡流程的 mermaid 流程图
graph LR
A[开始] --> B[编写 XDP 程序];
B --> C[设置后端服务器 IP 地址];
C --> D[加载 XDP 程序到网卡];
D --> E[数据包到达网卡];
E --> F[使用轮询算法选择后端服务器];
F --> G[修改数据包目标 IP 地址];
G --> H[重新传输数据包];
H --> I[结束];
8. 总结与展望
通过以上详细的介绍,我们深入了解了 XDP 程序的加载、测试、应用场景以及具体的实现流程。XDP 在网络监控、DDoS 缓解和负载均衡等方面展现出了巨大的优势,能够帮助我们更高效地处理网络数据包。
在未来,随着网络技术的不断发展,XDP 有望在更多的领域得到应用,例如软件定义网络(SDN)、网络功能虚拟化(NFV)等。同时,我们也可以进一步优化 XDP 程序的性能,提高其处理数据包的效率。
希望本文能够为你在使用 XDP 进行网络编程方面提供有价值的参考。如果你有任何问题或建议,欢迎在评论区留言讨论。
超级会员免费看
1838

被折叠的 条评论
为什么被折叠?



