一、 概述
上节对DDoS各种类型的攻击进行了阐述,后续我们将结合各种类型报文到达服务器后的源码处理过程,深入分析各种类型报文的Flood流量对服务器的危害。SYN/ACK/UDP/ICMP类的攻击,侧重分析其在TCP协议栈的行为;应用层类的攻击,则侧重其在对应服务器的行为,如CC攻击(HTTP),侧重分析其在Nginx等服务器的行为;DNS攻击,则分析其在bind等服务器的行为。让我们首先从SYN Flood开始。
二、 目标服务器环境
后续的TCP协议栈是基于linux5.4源码进行分析
三、 协议栈的SYN 报文处理
- 函数调用栈
通过ftrace,得到SYN报文进入TCP协议栈的函数调用过程及处理时间如下
| tcp_v4_rcv() {
| __inet_lookup_established() {
0.347 us | inet_ehashfn();
1.067 us | }
| __inet_lookup_listener() {
0.733 us | inet_lhash2_lookup();
| inet_lhash2_lookup() {
0.265 us | inet_ehashfn();
0.938 us | reuseport_select_sock();
2.980 us | }
4.515 us | }
| tcp_v4_inbound_md5_hash() {
0.349 us | tcp_parse_md5sig_option();
0.950 us | }
| sk_filter_trim_cap() {
0.817 us | __cgroup_bpf_run_filter_skb();
| security_sock_rcv_skb() {
0.244 us | apparmor_socket_sock_rcv_skb();
1.255 us | }
3.253 us | }
0.263 us | tcp_v4_fill_cb();
| tcp_v4_do_rcv() {
| tcp_rcv_state_process() {
| tcp_v4_conn_request() {
| tcp_conn_request() {
| inet_reqsk_alloc() {
| kmem_cache_alloc() {
0.240 us | should_failslab();
0.260 us | memcg_kmem_get_cache();
0.491 us | memcg_kmem_put_cache();
2.876 us | }
3.489 us | }
0.498 us | tcp_parse_options();
0.263 us | tcp_v4_init_req();
0.339 us | security_inet_conn_request();
| tcp_v4_init_ts_off() {
0.788 us | secure_tcp_ts_off();
2.402 us | }
| tcp_v4_route_req() {
| inet_csk_route_req() {
0.240 us | security_req_classify_flow();
| ip_route_output_flow() {
| ip_route_output_key_hash() {
| ip_route_output_key_hash_rcu() {
| __ip_dev_find() {
0.484 us | inet_lookup_ifaddr_rcu();
1.042 us | }
1.016 us | fib_table_lookup();
0.256 us | fib_select_path();
0.247 us | find_exception();
5.207 us | }
5.724 us | }
| xfrm_lookup_route() {
0.417 us | xfrm_lookup_with_ifid();
1.008 us | }
7.700 us | }
8.955 us | }
9.535 us | }
| tcp_v4_init_seq() {
| secure_tcp_seq() {
0.368 us | ktime_get_with_offset();
1.107 us | }
1.682 us | }
| tcp_openreq_init_rwin() {
| ipv4_default_advmss() {
0.515 us | ipv4_mtu();
1.141 us | }
0.356 us | __cgroup_bpf_run_filter_sock_ops();
0.287 us | tcp_select_initial_window();
3.273 us | }
0.323 us | tcp_reqsk_record_syn();
0.371 us | tcp_try_fastopen();
0.300 us | __cgroup_bpf_run_filter_sock_ops();
| inet_csk_reqsk_queue_hash_add() {
0.244 us | init_timer_key();
| mod_timer() {
| lock_timer_base() {
0.343 us | _raw_spin_lock_irqsave();
0.870 us | }
0.235 us | detach_if_pending();
| __internal_add_timer() {
0.253 us | calc_wheel_index();
0.314 us | enqueue_timer();
1.376 us | }
0.251 us | trigger_dyntick_cpu.isra.0();
0.242 us | _raw_spin_unlock_irqrestore();
4.688 us | }
| inet_ehash_insert() {
0.262 us | inet_ehashfn();
0.455 us | _raw_spin_lock();
1.611 us | }
7.768 us | }
| tcp_v4_send_synack() {
| tcp_make_synack() {
| __alloc_skb() {
| kmem_cache_alloc_node() {
0.242 us | should_failslab();
0.368 us | memcg_kmem_put_cache();
1.984 us | }
| __kmalloc_reserve.isra.0() {
| __kmalloc_node_track_caller() {
0.258 us | kmalloc_slab();
0.239 us | should_failslab();
0.290 us | memcg_kmem_put_cache();
2.707 us | }
3.278 us | }
| ksize() {
0.760 us | __ksize();
1.255 us | }
7.658 us | }
0.270 us | skb_set_owner_w();
| ipv4_default_advmss() {
0.339 us | ipv4_mtu();
0.796 us | }
0.219 us | ktime_get();
0.196 us | tcp_v4_md5_lookup();
0.590 us | skb_push();
0.344 us | tcp_options_write();
+ 12.570 us | }
0.229 us | __tcp_v4_send_check();
| ip_build_and_send_pkt() {
0.240 us | skb_push();
| ip_local_out() {
| __ip_local_out() {
0.234 us | ip_send_check();
0.824 us | }
| ip_output() {
| ip_finish_output() {
0.289 us | __cgroup_bpf_run_filter_skb();
| __ip_finish_output() {
0.260 us | ipv4_mtu();
| ip_finish_output2() {
| dev_queue_xmit() {
| __dev_queue_xmit() {
0.334 us | dst_release();
0.322 us | netdev_core_pick_tx();
0.242 us | _raw_spin_lock();
| sch_direct_xmit() {
| validate_xmit_skb_list() {
| validate_xmit_skb() {
| netif_skb_features() {
0.249 us | skb_network_protocol();
1.077 us | }
0.250 us | skb_csum_hwoffload_help();
0.243 us | validate_xmit_xfrm();
3.001 us | }
3.554 us | }
0.244 us | _raw_spin_lock();
| dev_hard_start_xmit() {
| e1000_xmit_frame e1000 {
0.257 us | e1000_maybe_stop_tx e1000;
| e1000_tx_map e1000 {
0.365 us | dma_direct_map_page();
1.060 us | }
0.303 us | skb_clone_tx_timestamp();
0.241 us | e1000_maybe_stop_tx e1000;
+ 25.036 us | }
+ 25.973 us | }
0.257 us | _raw_spin_lock();
+ 31.443 us | }
| __qdisc_run() {
0.300 us | fq_codel_dequeue sch_fq_codel;
1.535 us | }
0.246 us | __local_bh_enable_ip();
+ 37.852 us | }
+ 38.431 us | }
0.242 us | __local_bh_enable_ip();
+ 40.742 us | }
+ 41.934 us | }
+ 43.050 us | }
+ 43.668 us | }
+ 45.433 us | }
+ 46.662 us | }
+ 60.682 us | }
+ 96.262 us | }
+ 96.863 us | }
0.302 us | __local_bh_enable_ip();
| consume_skb() {
| skb_release_all() {
0.284 us | skb_release_head_state();
| skb_release_data() {
| skb_free_head() {
0.512 us | kfree();
1.178 us | }
1.862 us | }
3.113 us | }
| kfree_skbmem() {
1.015 us | kmem_cache_free();
1.773 us | }
5.743 us | }
! 104.297 us | }
! 105.184 us | }
! 118.677 us | }
关键函数的分析,链接里的文章分析得比较清楚,感兴趣的读者可以参考,这里就不再赘述。
根据图表可以看到,tcp_v4_rcv的处理时间大概在118微秒,那么内核1s可以处理的SYN报文为10^6 / 118 = 8474。仅从处理时间上来看,我们如果对目标服务器每秒发送超过8474个SYN报文,TCP协议栈就要开始丢包了。
- 资源分配
SYN报文涉及到的资源分配主要包含
a. 存储SYN报文信息的struct sk_buff
b. 发送SYN/ACK报文信息的struct sk_buff
c. 存储请求信息的struct request_sock
d. 创建重传定时器
其中存储SYN报文信息的sk_buff和存储SYN/ACK报文信息的sk_buff, 在tcp_v4_rcv处理完成后,就都释放了。存储请求信息的request_sock, 结局有两个:一是在完成三次握手后,挂到更大的结构体struct sock下,二是连接超时后释放(tcp连接超时重传时,需要该结构体)。而定时器则会在超时后,重新发送SYN/ACK报文,白白浪费设备性能。 struct request_sock结构体的大小为224字节,而申请该结构体时是从request_sock_TCP slab中申请,该slab中的元素结构体大小为304字节。1G内存就可以支持300万的结构体分配,所以对当前服务器配置来讲, 当SYN Flood到达服务器后,消耗的主要是CPU资源,而不是内存资源。 当然,SYN Flood最大可能是直接把带宽堵死,服务器连处理正常SYN请求的机会都不会有,由于这里主要探讨其对TCP协议栈的影响,所以没有展开。
四、 协议栈的SYN Flood防护
TCP协议栈本身有针对SYN Flood的SYN Cookie算法,启用SYN Cookie算法
后,多了以下函数调用生成序列号
0) | cookie_v4_init_sequence() {
0) | __cookie_v4_init_sequence() {
0) 0.226 us | cookie_hash();
0) 0.196 us | cookie_hash();
0) 0.898 us | }
0) 1.256 us | }
该算法的核心思想为:
- 设T是随时间增长的计数器(每分钟增加一次)
- 设M为客户端发送的SYN/ACK报文中的MSS选项值
- 设S是连接的四元组和T经过Hash的值,即S = Hash(sip, dip, sport, dport, T),这就是服务器回复的SYN/ACK的序列号(序列号里还存储有MSS信息,这里不再展开)
- 发送完成后,不再需要定时器和存储请求信息的struct request_sock
- 当ACK报文到达后,通过对ACK 序列号的校验,确认连接是否建立成功。
这样,协议栈的防护方案,在资源层面,节约了struct request_sock和 timer资源,但整个报文的处理时间却无法提升。该方法也只能用于防护虚假源IP的SYN Flood防护,对真实连接的SYN Flood攻击,却无能为力。
五、 网宿的SYN Flood防护
可以看到,协议栈本身的SYN Flood防护较弱,当碰到大流量SYN Flood攻击时,协议栈早早的就开始丢包,甚至罢工了事。网宿的SYN Flood防护,将报文直接从网卡接收到用户态,单SYN报文的处理时间可以达到0.201微秒,且不需要存储任何临时信息,无论是处理性能,还是资源占用,都有质的飞跃。对真实连接的SYN Flood攻击,也有多种防护方案,如分布式防护,代理连接,源IP新建连接限制,协议分析等。
六、 参考链接
1.https://blog.csdn.net/wangquan1992/article/details/108903813
2.https://blog.csdn.net/wangquan1992/article/details/108908194
3.https://wangquan.blog.csdn.net/article/details/108914464
4.https://segmentfault.com/a/1190000019292140