文章内容为学习整理笔记,绝大部分来源于 倪朋飞 老师的 Linux性能优化实战

一、基础概念

  DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。
  从攻击的原理上来看,DDoS 可以分为下面几种类型:
   • 耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
   • 耗尽操作系统的资源。网络服务的正常运行,都需要一定的系统资源,像是 CPU、内存等物理资源,以及连接表等软件资源。一旦资源耗尽,系统就不能处理其他正常的网络连接。
   • 消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。

二、SYN Flood

  SYN Flood,TCP 洪水攻击,即客户端发送大量伪造的 TCP 连接请求,这是互联网中最经典的 DDoS 攻击方式,当然这种攻击比较古老了,很多设备、软件都自带有防范措施。
  原理:即客户端构造大量的 SYN 包,请求建立 TCP 连接(常用假冒的 IP 或 IP 段);服务器收到包后,会向源 IP 发送 SYN+ACK 报文,并等待三次握手的最后一次 ACK 报文,直到超时。
  因为对方是假冒IP,对方永远收不到包且不会回应。导致被攻击服务器保持大量 SYN_RECV 状态的“半开连接”,由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的 TCP 连接,导致正常的业务请求连接不进来。
  并且如果收不到客户端的响应,系统默认是会重试 5 次发送 SYN+ACK 报文,每次重传大约等待30~40秒,即会占用连接大约180秒。

我们模拟下,现有3台机器,角色如下:

• 10.0.0.10:web服务
• 10.0.0.11:正常客户端
• 10.0.0.110:攻击者

先查看web服务器正常网络连接,然后客户端正常访问一下查看响应码和响应时间。

# web服务器
[root@web01 ~]# netstat -ant|awk '/^tcp/{++S[$NF]} END {for(key in S) print (key,S[key])}'
LISTEN 8
ESTABLISHED 1
TIME_WAIT 1

# 客户端
[root@client ~]# curl -Lsw 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null 10.0.0.10
Http code: 200
Total time:0.001s

开始模拟 SYN Flood 攻击,这里我们用到 hping3:

[root@test ~]# hping3  -d 150 -w 64 -p 80 -S --flood --rand-source 10.0.0.10

选项说明:
 -c:发送数据包的个数
 -d:每个数据包的大小
 -S:发送SYN数据包
 -w:TCP窗口大小
 -p:目标端口,你可以指定任意端口
 -i u1:表示一微秒发一次 
 --flood:尽可能快的发送数据包
 --rand-source:使用随机性的源头IP地址。你还可以使用-a或–spoof来隐藏主机名

  到 web 服务器上观测,可以发现此时 SYN_RECV 状态的连接达到 256,并且有大量的 SYN 包被丢弃,即 TCP 半连接队列溢出,说明服务器此时收到了大量的 SYN 包。

# 服务端
[root@web01 ~]# netstat -ant|awk '/^tcp/{++S[$NF]} END {for(key in S) print (key,S[key])}'
LISTEN 8
SYN_RECV 256
ESTABLISHED 1
[root@web01 ~]# netstat -s | grep -i syn
    5367 resets received for embryonic SYN_RECV sockets
    733042 SYNs to LISTEN sockets dropped
    TCPSynRetrans: 1199
[root@web01 ~]# netstat -s | grep -i syn
    6462 resets received for embryonic SYN_RECV sockets
    978476 SYNs to LISTEN sockets dropped
    TCPSynRetrans: 1433

# 客户端。直接连接超时,收不到web服务响应。
[root@web02 ~]# curl -Lsw 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null 10.0.0.10
Http code: 000
Total time:127.165s

  再用 sar 查看下系统当前的网络吞吐量和 PPS。可以看到,网络接收的 PPS(每秒收发的报文数)已经达到了 50000 多,但是 BPS(每秒收发的字节数) 却只有 10000 kB 多点,这样每个包的大小就只有 200 B 左右,明显为小包。

[root@web01 ~]# sar -n DEV 1 2
Linux 3.10.0-1160.41.1.el7.x86_64 (web01)   09/03/2021  _x86_64_    (1 CPU)

10:18:36 AM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
10:18:37 AM     ens34      0.00      0.00      0.00      0.00      0.00      0.00      0.00
10:18:37 AM        lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00
10:18:37 AM     ens33  55476.92    694.87  10966.95     40.87      0.00      0.00      0.00

10:18:37 AM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
10:18:38 AM     ens34      0.00      0.00      0.00      0.00      0.00      0.00      0.00
10:18:38 AM        lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00
10:18:38 AM     ens33  53110.00    232.50  10563.63     14.15      0.00      0.00      0.00

Average:        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
Average:        ens34      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:        ens33  54278.48    460.76  10762.74     27.34      0.00      0.00      0.00

我们抓下包看看,Flags [S] 表示这是一个 SYN 包,可以发现被大量的SYN 包信息刷屏。

[root@web01 ~]# tcpdump -i ens33 -n tcp port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
10:29:46.346958 IP 162.225.52.132.21255 > 10.0.0.10.http: Flags [S], seq 626935353:626935503, win 64, length 150: HTTP
10:29:46.346960 IP 213.198.216.68.21256 > 10.0.0.10.http: Flags [S], seq 941293052:941293202, win 64, length 150: HTTP
10:29:46.347003 IP 3.60.7.7.21257 > 10.0.0.10.http: Flags [S], seq 159390291:159390441, win 64, length 150: HTTP
10:29:46.347005 IP 20.174.214.29.21258 > 10.0.0.10.http: Flags [S], seq 1682425979:1682426129, win 64, length 150: HTTP
10:29:46.347056 IP 37.68.31.59.21259 > 10.0.0.10.http: Flags [S], seq 258849847:258849997, win 64, length 150: HTTP
10:29:46.347058 IP 193.238.163.202.21260 > 10.0.0.10.http: Flags [S], seq 1355259239:1355259389, win 64, length 150: HTTP
10:29:46.347118 IP 41.118.182.140.21261 > 10.0.0.10.http: Flags [S], seq 1428945424:1428945574, win 64, length 150: HTTP
......

对于这种情况,我们可以调整内核参数来缓解。
内核参数(下面修改都是临时有效,永久需要写入/etc/sysctl.conf文件中):
调整每个 SYN_RECV 连接的失败重试次数,上面已经解释了就不在赘述了。

[root@web01 ~]# sysctl net.ipv4.tcp_synack_retries
net.ipv4.tcp_synack_retries = 5

[root@web01 ~]# sysctl -w net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_synack_retries = 1

  开启TCP SYN Cookies。SYN Cookies 是基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为 cookie。
  然后,这个 cookie 就被用作序列号,来应答 SYN+ACK 包,并释放连接状态。当客户端发送完三次握手的最后一次 ACK 后,服务器就会再次计算这个哈希值,确认是上次返回的 SYN+ACK 的返回包,才会进入 TCP 的连接状态。所以在完成三次握手前是不会为任何一个连接分配任何资源的。
  因而开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。注意,开启 TCP syncookies 后,内核选项 net.ipv4.tcp_max_syn_backlog 也就无效了。

[root@web01 ~]# sysctl net.ipv4.tcp_syncookies
net.ipv4.tcp_syncookies = 0

[root@web01 ~]# sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1

半连接队列长度,可能有些配置了不生效,因为这个参数和内核版本有关。

[root@web01 ~]# sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 256

# 涉及相关参数
net.core.somaxconn

iptables,针对单个IP有效,像上面这种不停随机来源IP无效。

# 限制syn并发数为每秒1次
iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT

# 限制单个IP在60秒新建立的连接数为10
iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT

三、DDoS 如何防御

  实际上,当 DDoS 报文到达服务器后,Linux 提供的机制只能缓解,而无法彻底解决。即使像是 SYN Flood 这样的小包攻击,其巨大的 PPS ,也会导致 Linux 内核消耗大量资源,进而导致其他网络报文的处理缓慢。虽然你可以调整内核参数,缓解 DDoS 带来的性能问题,但也无法彻底解决它。
  在Linux 内核中冗长的协议栈,在 PPS 很大时,就是一个巨大的负担。对 DDoS 攻击来说,也是一样的道理。我们可以基于 XDP 或者 DPDK,构建 DDoS 方案,在内核网络协议栈前,或者跳过内核协议栈,来识别并丢弃 DDoS 报文,避免 DDoS 对系统其他资源的消耗。
  不过,对于流量型的 DDoS 来说,当服务器的带宽被耗尽后,在服务器内部处理就无能为力了。这时,只能在服务器外部的网络设备中,设法识别并阻断流量(当然前提是网络设备要能扛住流量攻击)。可以购置专业的入侵检测和防御设备(如阿里云的DDOS高防),配置流量清洗设备或者购买运营商的清洗服务器阻断恶意流量等。
  既然 DDoS 这么难防御,这是不是说明, Linux 服务器内部压根儿就不关注这一点,而是全部交给专业的网络设备来处理呢?
  当然不是,因为 DDoS 并不一定是因为大流量或者大 PPS,有时候,慢速的请求也会带来巨大的性能下降(这种情况称为慢速 DDoS)。
  比如,很多针对应用程序的攻击,都会伪装成正常用户来请求资源。这种情况下,请求流量可能本身并不大,但响应流量却可能很大,并且应用程序内部也很可能要耗费大量资源处理。这时,就需要应用程序考虑识别,并尽早拒绝掉这些恶意流量,比如合理利用缓存、增加 WAF(Web Application Firewall)、使用 CDN 等等。

发表评论

验证码: 59 − 49 =