背景
最近公司遇到的ddos攻击,老板给了一篇文章(googleXDP,找一个pdf)及一个名词XDP让我去调研这个技术的实现方案及原理。
技术介绍
什么是DDOS:
分布式拒绝服务(Distributed Denial of Service,简称DDoS)将多台计算机联合起来作为攻击平台,通过远程连接利用恶意程序,对一个或多个目标发起DDoS攻击,消耗目标服务器性能或网络带宽,从而造成服务器无法正常地提供服务。
技术角度:
1客户端在三次握手过程中只发syn包,服务器分配资源并把该链接放到半链接队列同时回复syn-ack包,后续客户端不理会该次链接。导致服务器:a.半连接队列满,导致正常请求链接被服务器丢弃;b.大量无用的链接消耗服务端资源;
2目前服务器默认会开启syncookie防护,当服务器超过一定链接时,默认开启syncookie功能,该功能的主要作用是针对1中出现的情况而定制的。开启该功能后,客户端发起syn后,服务器此时并不会为当前链接立马分配资源,而是通过一系列的算法生成一个cookie数值,通过syn-ack包发给客户端,当正确的客户端携带cookie资源发起三次握手后的最后一次ack时,服务器校验通过后才会真正的把这条链接认定为合法链接进而为该链接分配资源;
3听起来2已经可以完全解决DDOS的问题啊,其实不然,因为在生成cookie和验证cookie的时候是非常消耗CPU算力的。如果有大量的攻击上来,那么服务器的CPU资源就会消耗在cookie的生成及验证上,无暇处理正常的请求或者降低了服务器的服务能力。
那么怎么解决呢? 开始提到的XDP到底是做什么用的呢?如何才能把CPU的资源释放出来呢?
什么是XDP:
XDP是Linux网络路径上内核集成的数据包处理器,具有安全、可编程、高性能的特点。当网卡驱动程序收到数据包时,该处理器执行BPF程序。XDP可以在数据包进入协议栈之前就进行处理,因此具有很高的性能,可用于DDoS防御、防火墙、负载均衡等领域。
我们来看一下XDP在内核中处于什么位置:
可以很明显的看到XDP是最接近网卡层面的,如果把cookie数据的处理放到XDP上,那么就可以释放CPU的资源了。
好了,下面就是搞一下XDP程序了。
源码调试
由于需要将XDP程序挂在到内核上,所以相应的程序编译及处理都需要按照内核的要求来处理,如果通过编译内核的方法来编译XDP程序会让你疯掉(只在内核上开启BTF配置项就浪费了我两天时间)。万幸的是我们还有clang、iprout2、llvm等
XDP程序的编译 --- clang
clang -target bpf -c xdp.c -o xdp.o
XDP程序加载及卸载 ---iproute2、libbpf
curl -vo master.zip 'https://github.com/libbpf/libbpf/archive/refs/heads/master.zip' -A'chrome' -L
cd libbpf/src
make
make install
git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git
./configure --libbpf_force=on
make
make install
加载程序:
ip link set enp26s0 xdp object xdp.o sec .text
查看程序:
ip a show enp26s0
卸载程序:
ip a show enp26s0
XDP程序主体流程大体如下:
1.获取网络数据流
2.解析数据链路层数据,获取IP协议数据
3.解析ip层数据,获取TCP协议数据
4.主要目的是为了解析网络层数据,获取到TCP-flags的状态, 如果发现syn-ack包及ack包时,直接计算cookie并在下一个流程中验证cookie,如果验证失败,直接在网卡层就将该包丢弃,等待正确的客户段进行建联
解析数据链路层数据:
struct ethhdr* ether = packet->ether;
if (ether->h_proto != bpf_ntohs(ETH_P_IP)) { //非IP协议直接放行
return XDP_PASS;
}
解析IP层数据
struct iphdr* ip = packet->ip;
if (ip->protocol != IPPROTO_TCP) { //非TCP协议直接放行
return XDP_PASS;
}
处理网络层数据
struct tcphdr* tcp = packet->tcp;
switch (bpf_ntohs(tcp->flags) & (TH_SYN | TH_ACK)) {
case TH_SYN:
return process_tcp_syn(packet); //对于syn-ack包需要计算syncookie
case TH_ACK:
return process_tcp_ack(packet);//对于ack包需要校验客户端携带的cookie是否一致
default:
return XDP_PASS;
}
XDP内核代码框架
#include <linux/bpf.h>
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
//do your things
return XDP_PASS;
}
char _license[] SEC("license") = "GPL"; //定义声明 要不加内核加载有问题
内核版本从2.3x的版本就开始支持XDP,到目前为止,支持的情况已经比较好了。
但是需要将XDP(c代码编写)加载到网卡上需要特殊的编译流程,而且内核对XDP的代码要求比较严格,会事先预编译代码,查看代码是否存在循环及异常内存操作等。
XDP程序执行结束后会返回一个结果,告诉调用者接下来如何处理这个包:
- XDP_DROP,丢弃这个包,主要用于报文过滤的安全场景;
- XDP_PASS,将这个包“交给/还给”内核,继续走正常的内核处理流程;
- XDP_TX,从收到包的网卡上再将这个包发出去(即hairpin模式),主要用于负载均衡场景;
- XDP_REDIRECT,何XDP_TX类似,但是是通过另外一个网卡将包发出去。除此之外还可以实现将报文重定向到其他的CPU处理,类似于XDP_PASS继续走内核处理流程,但是由其他的CPU处理,当前CPU继续处理后续的报文接收;
- XDP_ABORTED,表示程序产生异常,行为类似XDP_DROP,但是会通过一个tracepoint打印日志义工追踪;
禁止PING的XDP程序
程序源码
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("icmp_drop")
int dropper(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;
ipsize = sizeof(*eth);
struct iphdr *ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_PASS;
}
if (ip->protocol == IPPROTO_ICMP) {
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
测试验证
ping 221.214.0.38
PING 221.214.0.38 (221.214.0.38) 56(84) bytes of data.
^C
--- 221.214.0.38 ping statistics ---
10 packets transmitted, 0 received, 100% packet loss, time 9250ms
synproxy
但是XDP虽然替代完成了三次握手,但是最重要的一点: XDP面向无连接的(除非配合synproxy等有状态的路由使用)
需要synproxy建立三次握手,开启synproxy指令:
sysctl -w net.netfilter.nf_conntrack_tcp_loose=0
iptables -t raw -t PREROUTING -i eth0 -p tcp -m tcp --syn --dport 80 -j CT --notrack
iptables -A FORWARD -i eth0 -p tcp -m tcp --dport 80 -m state --state INVALID,UNTRACKED -j SYNPROXY \
--timestamp --sack-perm --wscale 7 --mss 1460
iptables -A FORWARD -m state --state INVALID -j DROP
===
iptables -t raw -A PREROUTING -i eth0 -p tcp -m tcp --syn --dport 80 -j CT --notrack
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state UNTRACKED,INVALID -j SYNPROXY --sack-perm --timestamp --wscale 7 --mss 1460
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state INVALID -j DROP
展望
未来可期