TCP半连接队列和全连接

 

w概述

 
  如上图所示, 在TCP三次握手中,服务器维护一个半连接队列(sync queue) 和一个全连接队列(accept queue)。 
当服务端接收到客户端第一次SYN握手请求时,将创建的request_sock结构,存储在半连接队列中(向客户端发送SYN+ACK,并期待客户端响应ACK),此时的连接在服务器端出于SYN_RECV状态。当服务端收到客户端最后的ACK确认时,将半连接中的相应条目删除,然后将相应的连接放入 全连接队列中, 此时服务端连接状态为ESTABLISHED。 进入全连接队列中的连接等待accept()调用取用。 

  既然是队列,肯定就有大小,那么当这两个队列满了没有空间了怎么办呢? 例如如果我们listen()后不去accept() ,那么全连接队列肯定会满的。 我们下面分别对于这两个队列结合试验进行描述。

试验环境: 

CentOS Linux release 7.5.1804 (Core)
Linux version 3.10.0-229.4.2.el7.x86_64

syns queue 半连接队列

  首先说一下 SYN flooding攻击,为了应对SYN flooding(即客户端只发送SYN包发起握手而不回应ACK完成连接建立,快速填满server端的半连接队列,让它无法处理正常的握手请求),Linux实现了一种称为SYNcookie的机制,通过net.ipv4.tcp_syncookies控制,设置为1表示开启。简单说SYNcookie就是将连接信息编码在ISN(initialsequencenumber)中返回给客户端,这时server不需要将半连接保存在队列中,而是利用客户端随后发来的ACK带回的ISN还原连接信息,以完成连接的建立,避免了半连接队列被攻击SYN包填满。 也就是说,如果开启了syncookies的话(通过 TCP参数 net.ipv4.tcp_syncookies配置 ),半连接队列就相当于是无限大的了。在我的环境中就是默认开启的。 

  如果我们将syncookies关闭的话,半连接队列的长度将为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) ,,此时对半连接填满时的处理策略是 server将 丢弃请求连接的SYN,不回复SYN+ACK,这样就会造成client收不到握手响应,始终处在SYN_SENT状态,经过几次重传后,客户端 connect() 调用失败。

accept queue 全连接队列

  全连接队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128(/proc/sys/net/core/somaxconn),表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则是由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以有我们的应用程序去定义。 当全连接队列满了后的处理策略基于TCP参数net.ipv4.tcp_abort_on_overflow,在我的机器上默认为0。 

   1.tcp_abort_on_overflow 关闭时

  当server收到最后一次ACK时,希望将连接从半连接队列中取出放入全连接队列,但是此时全连接队列已满,此时的策略是 将最后接收到的ACK丢弃,并且根据net.ipv4.tcp_synack_retries定义的次数重新向client发送SYN+ACK, client在接收到重传的SYN+ACK后会认为之前的ACK丢失了进而重传ACK,这样在下次重新接收到ACK后,如果全连接队列有空间了,连接就可以正确完成建立。 如果重传了规定次数后全连接队列中依旧没有空间,那么server会简单终止这次连接(这里简单终止的意思是server并没有像client发送RST表明连接无法建立,而是直接丢弃了,这样就会导致在client中的连接处在ESTABLISHED状态,并一直如此,后面的实验会有涉及,我很困惑为什么要这样设计? 还是我没有正确理解 !)。

   2.tcp_abort_on_overflow 开启时

  在收到握手的最后一次ACK后,在全连接中如果没有空间,直接向client回复RST,表示连接无法建立。 

实验

  下面的server代码在listen()之后就不再accept,这就相当于全连接队列不再有消费者,很快就会填满。

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <sys/socket.h>
 4 #include <sys/time.h>
 5 #include <netinet/in.h>
 6 #include <arpa/inet.h>
 7 #include <errno.h>
 8 #include <stdlib.h>
 9 #include <string.h>
10 #include <unistd.h>
11 
12 #define PORT 8888
13 #define BACKLOG 3 
14 
15 int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen) {
16     int fd;
17     int err = 0;
18 
19     if ((fd = socket(addr->sa_family, type, 0)) < 0)
20         return(-1);
21     if (bind(fd, addr, alen) < 0)
22         goto errout;
23     if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
24         if (listen(fd, qlen) < 0)
25             goto errout;
26     }
27 
28     return(fd);
29 
30 errout:
31     err = errno;
32     close(fd);
33     errno = err;
34     return(-1);
35 }
36 
37 
38 void my_err(const char* msg, int line) {
39     fprintf(stderr, "line: %d", line);
40     perror(msg);
41 }
42 
43 
44 int main(int argc, char *argv[])
45 {
46     int conn_len;
47     int sock_fd, conn_fd;
48     struct sockaddr_in serv_addr, conn_addr;
49 
50     if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
51         my_err("socket", __LINE__);
52         exit(1);
53     }
54 
55     memset(&serv_addr, 0, sizeof(struct sockaddr_in));
56     serv_addr.sin_family = AF_INET;
57     serv_addr.sin_port = htons(PORT);
58     serv_addr.sin_addr.s_addr = htons(INADDR_ANY);
59 
60     if (bind(sock_fd, (struct sockaddr *) &serv_addr, sizeof(struct sockaddr_in)) < 0) {
61         my_err("bind", __LINE__);
62         exit(1);
63     }
64 
65     if(listen(sock_fd, BACKLOG) < 0) {
66         my_err("sock", __LINE__);
67         exit(1);
68     }
69 
70     while(1) {}
71 }
server.c

  下面的client代码会起多个线程来连接server端,前面的server代码中并没有调用accept(),你可能会期待所有连接都会失败,但事实却不尽如此!

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <sys/socket.h>
 4 #include <netinet/in.h>
 5 #include <arpa/inet.h>
 6 #include <string.h>
 7 #include <strings.h>
 8 #include <stdlib.h>
 9 #include <unistd.h>
10 #include <pthread.h>
11 
12 #define PORT 8888
13 #define thread_num 5
14 
15 struct sockaddr_in serv_addr;
16 
17 void *func() {
18     int conn_fd;
19     conn_fd = socket(AF_INET, SOCK_STREAM, 0);
20     printf("conn_fd : %d\n", conn_fd);
21 
22     if (connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0) {
23         printf("connect error\n");
24     }
25     
26     while(1) {}
27 }
28 
29 int main(int argc, char *argv[])
30 {
31     memset(&serv_addr, 0, sizeof(struct sockaddr_in));
32     serv_addr.sin_family = AF_INET;
33     serv_addr.sin_port = htons(PORT);
34     // 4.14就是我的server
35     inet_aton("192.168.4.14", (struct in_addr *)&serv_addr.sin_addr);
36 
37 
38     int retval;
39 
40     pthread_t pid[thread_num];
41     for (int i = 0; i < thread_num; ++i) {
42         pthread_create(&pid[i], NULL, &func, NULL);
43     }
44 
45     for (int i = 0; i < thread_num; i++) {
46         pthread_join(pid[i], (void *)&retval);
47     }
48 
49     return 0;
50 }
client.c

 

  我们在前面已经讨论了影响这两个队列的参数,先来看看我的机器默认配置吧。

# cat /proc/sys/net/ipv4/tcp_syncookies

# cat /proc/sys/net/ipv4/tcp_abort_on_overflow

# cat /proc/sys/net/ipv4/tcp_synack_retries

   我机器的默认配置开启了syncookies,就相当于半连接队列无限大了。 tcp_abort_on_overflow关闭了,那么在全连接满的时候会重传SYN+ACK,最后重传次数由tcp_synack_retries规定。 

  在server代码中listen的backlog参数传递的是3,所以全连接的队列大小应该为4,客户端代码一共发起了5次连接请求,那么肯定有一个请求最终无法进入全连接队列中(因为我们从来没有accept), 我们来看看会发生什么吧。(推荐一个将视频转gif的网站: https://convertio.co/zh/gif-converter/ )

  

 

   在server端, 五次请求四次最终出于ESTABLISHED状态,另外一个开始时出于 SYN_RECV状态,然后大约一分钟后,就只剩下四个ESTABLISHED的连接了。 在client端,五个连接都很快出于ESTABLISHED状态。 然后我关闭了server 进程,此时client端的四个ESTABLISHED的连接断开了,但是另外一个即便server端进程都不存在了,它依然是ESTABLISHED的,如果我不关闭client进程,这个连接将一直存在。

下面我们分析一下上面这个过程的数据包(在server端抓取):

首先我们分析四个正常加入全连接队列的连接,设置wireshark的过滤器: tcp.port==46820 (四个端口任意一个),如下图:

  前三次是正常的握手流程,双方最终都成为了ESTABLISHED状态,然后我关掉了server进程,进程结束前向client发送RST,表明意外断开连接。

然后我们将wireshark过滤器设置为 另外一个连接,在客户端,即使server进程已经被杀死,这个连接依旧是ESTABLISHED的,除非杀死client进程,否则它将一直如此。看下图:

 

  在server端收到客户端最后确认握手的ACK后,由于全连接队列中没有空间了,所以它向client端重传了SYN+ACK, 客户端在接收到重传的SYN+ACK后,认为之前的ACK丢失,并重传ACK,这个过程一直重复了 5次, 也就是图中黑色的10条数据。 在重传了5次后全队列依旧没有空间,server此时采取的动作是 直接丢弃了这个连接(对最后一次重传得到的客户端ACK不做任何处理,直接丢弃),这样客户端此时依旧会认为连接已经建立了,而实际上服务器端已经没有任何关于这条连接的任何信息了。 当我们杀死服务器端进程时,只有那四条成功的连接才会发送RST,而这条将得不到任何回复(我不太理解这么设计的原因!)

   下面我们修改默认配置,将 tcp_abort_on_overflow 打开,这样当全连接队列满了以后再接收到客户端最后一次握手的ACK后,server端应该会直接回复RST表示连接无法建立,一起来看看吧。

# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
# echo net.ipv4.tcp_abort_on_overflow=1 >> /etc/sysctl.conf
# sysctl -p
net.ipv4.tcp_abort_on_overflow = 1
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
1

修改完配置后我们仍然使用之前的代码,对于能够进入全连接队列的四个连接应该跟原来一样,但第五个连接在客户端因为接受到服务器RST的原因应该会很快断开,connect()方法失败返回,控制台打印出 connect error。

   同样在这个过程中我抓取了server端的数据包,我过滤掉了四个进入了全连接的连接。

  server端在接收到客户端最后一个ACK后,立即向客户端发送RST表示连接无法建立。

   上面我们分析了全连接的两种处理情况,下面继续讨论半连接。 我的机器上默认开启了syncookies,此时半连接可以认为是无限大的,所以也就不存在空间用完的情况。 我们先关闭 syncookies,然后tcp_max_syn_backlog的值设置为一个很小的值,这样 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)的值就应该是64, 也就是半连接队列的长度。 修改一下上面的client.c,将线程数改为100,那么就会有很多请求无法进入半连接队列,此时服务器对它们的处理策略是直接忽略掉,不会去响应SYN+ACK, 客户端在尝试几次重连后,就会认为连接无法建立。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 
 
 
 
 
 
 
 

 

转载于:https://www.cnblogs.com/zh1164/p/10003451.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值