39-案例篇:怎么缓解DDoS攻击带来的性能下降问题?
DDoS简介
DDoS的前身是DoS(Denail of Service)
即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求
DDoS(Distributed Denial of Service) 则是在DoS的基础上
采用了分布式架构,利用多台主机同时攻击目标主机
这样即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对
目前已知的最大流量攻击,正是Github遭受的DDoS攻击
其峰值流量已经达到了1.35Tbps,PPS更是超过了1.2亿(126.9 million)
从攻击的原理上来看,DDoS可以分为下面几种类型
- 第一种,耗尽带宽
无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限
带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文 - 第二种,耗尽操作系统的资源
网络服务的正常运行,都需要一定的系统资源,像是CPU、内存等物理资源,以及连接表等软件资源
一旦资源耗尽,系统就不能处理其他正常的网络连接 - 第三种,消耗应用程序的运行资源
应用程序的运行,通常还需要跟其他的资源或系统交互
如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应
比如构造大量不同的域名来攻击DNS服务器,就会导致DNS服务器不停执行迭代查询,并更新缓存
这会极大地消耗DNS服务器的资源,使DNS的响应变慢
案例
-
环境准备
Ubuntu 18.04 机器配置:2CPU,4GB内存 预先安装docker、sar、hping3、tcpdump、curl等工具 比如apt-get install docker.io hping3 tcpdump curl -y
其中一台虚拟机运行Nginx ,用来模拟待分析的Web服务器
而另外两台作为Web服务器的客户端,其中一台用作DoS攻击,而另一台则是正常的客户端
使用多台虚拟机的目的,自然还是为了相互隔离,避免“交叉感染”由于案例只使用了一台机器作为攻击源,所以这里的攻击实际上还是传统的DoS ,而非DDoS
-
在VM1,启动基本的nginx应用
# 运行Nginx服务并对外开放80端口 # --network=host表示使用主机网络(这是为了方便后面排查问题) root@alnk:~# docker run -itd --name=nginx --network=host nginx root@alnk:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 29df006db439 nginx "/docker-entrypoint.…" 21 seconds ago Up 19 seconds nginx
-
在VM2,VM3使用curl访问nginx端口
# -w表示只输出HTTP状态码及总时间,-o表示将响应重定向到/dev/null [root@local_chandao_192-168-1-8 ~]# curl -s -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null http://124.71.83.217/ Http code: 200 Total time:0.017s [root@local_chandao_192-168-1-5 ~]# curl -s -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null http://124.71.83.217/ Http code: 200 Total time:0.017s ## 正常情况下,访问Nginx只需要17ms(0.017s)
-
在VM2,运行hping3命令模拟DoS攻击
# -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80 # -i u10表示每隔10微秒发送一个网络帧 [root@local_chandao_192-168-1-5 ~]# hping3 -S -p 80 -i u10 124.71.83.217
-
回到终端一发现,现在不管执行什么命令都慢了很多
- 如果现象不那么明显,那么尝试把参数里面的u10调小(比如调成 u1),或者加上–flood选项
- 如果VM1完全没有响应了,那么适当调大u10(比如调成 u30),否则后面就不能通过SSH操作VM1
-
VM3执行下面的命令,模拟正常客户端的连接
[root@local_chandao_192-168-1-8 ~]# curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10 http://124.71.83.217 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- 0:00:03 --:--:-- 0Http code: 000 Total time:3.005s curl: (7) Failed connect to 124.71.83.217:80; 连接超时 ## 发现在VM3中,正常客户端的连接超时了,并没有收到Nginx服务的响应
-
VM1执行下面的命令
root@alnk:~# sar -n DEV 1 05:31:00 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 05:31:01 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 05:31:01 PM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 05:31:01 PM eth0 5397.00 2702.00 316.24 153.54 0.00 0.00 0.00 0.00 ## 从sar的输出中可以看到,网络接收的PPS已经达到了5000多,但是BPS却只有316kB 这样每个包的大小就只有64B(316*1024/5000=64) 这明显就是个小包了,不过具体是个什么样的包呢?
-
VM1执行下面的命令
# -i eth0 只抓取eth0网卡 # -n 不解析协议名和主机名 # tcp port 80表示只抓取tcp协议并且端口号为80的网络帧 root@alnk:~# tcpdump -i eth0 -n tcp port 80 17:34:18.784053 IP 192.168.0.53.80 > 113.89.232.48.11352: Flags [S.], seq 331757806, ack 2022207751, win 64240, options [mss 1460], length 0 17:34:18.784202 IP 113.89.232.48.59097 > 192.168.0.53.80: Flags [S], seq 2087575442, win 512, options [mss 536], length 0 17:34:18.784214 IP 192.168.0.53.80 > 113.89.232.48.59097: Flags [S.], seq 3057917146, ack 2087575443, win 64240, options [mss 1460], length 0 17:34:18.784830 IP 113.89.232.48.6072 > 192.168.0.53.80: Flags [S], seq 2139472643, win 512, options [mss 536], length 0 17:34:18.784845 IP 192.168.0.53.80 > 113.89.232.48.6072: Flags [S.], seq 1782419040, ack 2139472644, win 64240, options [mss ## 这个输出中Flags [S]表示这是一个SYN包 大量的SYN包表明,这是一个SYN Flood攻击
如果用上一节讲过的Wireshark来观察,则可以更直观地看到SYN Flood的过程
实际上,SYN Flood正是互联网中最经典的DDoS攻击方式- 客户端构造大量的SYN包,请求建立TCP连接
- 服务器收到包后,会向源IP发送SYN+ACK报文,并等待三次握手的最后一次ACK报文,直到超时
这种等待状态的TCP连接,通常也称为半开连接
由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的TCP连接参考下面这张TCP状态图,能看到此时服务器端的TCP连接,会处于SYN_RECEIVED状态
这其实提示了查看TCP半开连接的方法,关键在于SYN_RECEIVED状态的连接
可以使用netstat,来查看所有连接的状态
不过要注意,SYN_REVEIVED的状态,通常被缩写为SYN_RECV
-
VM1执行下面的命令
# -n表示不解析名字,-p表示显示连接所属进程 root@alnk:~# netstat -n -p |grep SYN_REC tcp 0 0 192.168.0.53:80 113.89.232.48:59922 SYN_RECV - tcp 0 0 192.168.0.53:80 113.89.232.48:55985 SYN_RECV - tcp 0 0 192.168.0.53:80 113.89.232.48:26796 SYN_RECV - tcp 0 0 192.168.0.53:80 113.89.232.48:36033 SYN_RECV -
从结果中可以发现大量SYN_RECV状态的连接,并且源IP地址为113.89.232.48
通过wc工具,来统计所有SYN_RECV状态的连接数root@alnk:~# netstat -n -p |grep SYN_REC|wc -l 193
找出源IP后,要解决SYN攻击的问题,只要丢掉相关的包就可以
这时,iptables可以完成这个任务,可以在终端一中,执行下面的iptables命令iptables -I INPUT -s 113.89.232.48 -p tcp -j REJECT
-
回到VM3,再次访问
[root@local_chandao_192-168-1-8 ~]# curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10 http://124.71.83.217 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 615 100 615 0 0 43887 0 --:--:-- --:--:-- --:--:-- 47307 Http code: 200 Total time:1.514s ## 可以发现正常用户也可以访问Nginx了,只是响应比较慢,从原来的2ms变成了现在的1.5s ## 不过一般来说,SYN Flood攻击中的源IP并不是固定的 比如,可以在hping3命令中,加入--rand-source选项,来随机化源IP 这时刚才的方法就不适用了
-
幸好还有很多其他方法,实现类似的目标,比如可以用以下两种方法,来限制syn包的速率
# 限制syn并发数为每秒1次 root@alnk:~# iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT # 限制单个IP在60秒新建立的连接数为10 root@alnk:~# iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT ## 到这里已经初步限制了SYN Flood攻击 不过这还不够,因为案例还只是单个的攻击源 ## 如果是多台机器同时发送SYN Flood,这种方法可能就直接无效了 因为很可能无法SSH登录(SSH也是基于TCP的)到机器上去,更别提执行上述所有的排查命令
-
需要事先对系统做一些TCP优化
# 比如SYN Flood会导致SYN_RECV状态的连接急剧增大 # 在上面的netstat命令中可以看到190多个处于半开状态的连接 # 半开状态的连接数是有限制的,执行下面的命令可以看到,默认的半连接容量只有256 root@alnk:~# sysctl net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_max_syn_backlog = 256 # SYN 数再稍微增大一些,就不能SSH登录机器了 # 所以应该增大半连接的容量,可以用下面的命令,将其增大为1024 root@alnk:~# sysctl -w net.ipv4.tcp_max_syn_backlog=1024 net.ipv4.tcp_max_syn_backlog = 1024 ## # 连接每个SYN_RECV时,如果失败的话,内核还会自动重试,并且默认的重试次数是5次 # 可以执行下面的命令,将其减小为1次 root@alnk:~# sysctl -w net.ipv4.tcp_synack_retries=1 net.ipv4.tcp_synack_retries = 1 ## # 除此之外TCP SYN Cookies也是一种专门防御SYN Flood攻击的方法 # SYN Cookies基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间) # 计算出一个哈希值(SHA1),这个哈希值称为cookie # 然后这个cookie就被用作序列号,来应答SYN+ACK包,并释放连接状态 # 当客户端发送完三次握手的最后一次ACK后,服务器就会再次计算这个哈希值 # 确认是上次返回的SYN+ACK的返回包,才会进入TCP的连接状态 # 因而开启SYN Cookies后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制 # 注意,开启TCP syncookies后,内核选项net.ipv4.tcp_max_syn_backlog也就无效了 ## 可以通过下面的命令,开启TCP SYN Cookies root@alnk:~# sysctl -w net.ipv4.tcp_syncookies=1 net.ipv4.tcp_syncookies = 1 ## # 注意,上述sysctl命令修改的配置都是临时的,重启后这些配置就会丢失 # 所以为了保证配置持久化,还应该把这些配置,写入/etc/sysctl.conf文件中 root@alnk:~# cat /etc/sysctl.conf net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_synack_retries = 1 net.ipv4.tcp_max_syn_backlog = 1024 # 写入/etc/sysctl.conf的配置,需要执行sysctl -p命令后,才会动态生效
DDoS到底该怎么防御
为什么不是解决DDoS ,而只是缓解呢?
而且今天案例中的方法,也只是让Nginx服务访问不再超时,但访问延迟还是比一开始时的2ms大得多实际上,当DDoS报文到达服务器后,Linux提供的机制只能缓解,而无法彻底解决
即使像是SYN Flood这样的小包攻击,其巨大的PPS
也会导致Linux内核消耗大量资源,进而导致其他网络报文的处理缓慢虽然可以调整内核参数,缓解DDoS带来的性能问题,却也会像案例这样,无法彻底解决它
在之前的C10K、C100K 文章中,提到过Linux内核中冗长的协议栈,在PPS很大时,就是一个巨大的负担
对DDoS攻击来说,也是一样的道理所以当时提到的C10M的方法,用到这里同样适合
比如可以基于XDP或者DPDK,构建DDoS方案,
在内核网络协议栈前,或者跳过内核协议栈,来识别并丢弃DDoS报文,避免DDoS对系统其他资源的消耗不过,对于流量型的DDoS来说,当服务器的带宽被耗尽后,在服务器内部处理就无能为力了
这时,只能在服务器外部的网络设备中,设法识别并阻断流量(当然前提是网络设备要能扛住流量攻击)
比如购置专业的入侵检测和防御设备,配置流量清洗设备阻断恶意流量等既然DDoS这么难防御,这是不是说明, Linux服务器内部压根儿就不关注这一点
而是全部交给专业的网络设备来处理呢?
当然不是,因为DDoS并不一定是因为大流量或者大 PPS
有时候,慢速的请求也会带来巨大的性能下降(这种情况称为慢速DDoS)比如很多针对应用程序的攻击,都会伪装成正常用户来请求资源
这种情况下,请求流量可能本身并不大,但响应流量却可能很大
并且应用程序内部也很可能要耗费大量资源处理这时就需要应用程序考虑识别,并尽早拒绝掉这些恶意流量
比如合理利用缓存、增加WAF(Web Application Firewall)、使用CDN等等
小结
今天学习了分布式拒绝服务(DDoS)时的缓解方法
DDoS利用大量的伪造请求,使目标服务耗费大量资源,来处理这些无效请求
进而无法正常响应正常的用户请求
由于DDoS的分布式、大流量、难追踪等特点,目前还没有方法可以完全防御 DDoS 带来的问题
只能设法缓解这个影响
比如可以购买专业的流量清洗设备和网络防火墙,在网络入口处阻断恶意流量
只保留正常流量进入数据中心的服务器中
在Linux服务器中,可以通过内核调优、DPDK、XDP等多种方法
来增大服务器的抗攻击能力,降低 DDoS 对正常服务的影响
而在应用程序中,可以利用各级缓存、 WAF、CDN等方式,缓解DDoS对应用程序的影响