w概述
既然是队列,肯定就有大小,那么当这两个队列满了没有空间了怎么办呢? 例如如果我们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 #include2 #include 3 #include 4 #include 5 #include in.h> 6 #include 7 #include 8 #include 9 #include <string.h> 10 #include 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 }
下面的client代码会起多个线程来连接server端,前面的server代码中并没有调用accept(),你可能会期待所有连接都会失败,但事实却不尽如此!


1 #include2 #include 3 #include 4 #include in.h> 5 #include 6 #include <string.h> 7 #include 8 #include 9 #include 10 #include 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 }
我们在前面已经讨论了影响这两个队列的参数,先来看看我的机器默认配置吧。
# cat /proc/sys/net/ipv4/tcp_syncookies
1
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
我机器的默认配置开启了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, 客户端在尝试几次重连后,就会认为连接无法建立。