[Network] Related experiments on TCP protocol

1. Understand the second parameter of listen

When writing server code for TCP sockets, after creating and binding the socket, you need to call the listen function to set the created socket as listening status, after which the server can call the accept function to obtain the established connection. Among them, the first parameter of the listen function is the socket that needs to be set to the listening state, and the second parameter of listen is generally set to 16, 32. 64, 128, but why should it be set to these values? listenWhat is the specific meaning of the second parameter of the function? Let's first look at the experimental results of the code and then explain why.

1. Experimental phenomena

The following code uses an experiment to illustrate the specific meaning of the second parameter of listen:

  • First write the server-side code of the TCP socket. When the server is initialized, the socket is created, bound, and monitored in sequence, but the accept function is not called after the server is initialized to obtain the underlying establishment. Good connection.
  • To facilitate verification, the second parameter of thelisten function is set to 1.
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

const int port = 8081;
const int num = 1;

int main()
{
    
    
	// 1. 创建监听套接字
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock < 0)
    {
    
    
		std::cerr << "socket error" << std::endl;
		return 1;
	}
	int opt = 1;
	setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	// 2. 绑定
	struct sockaddr_in local;
	memset(&local, 0, sizeof(local));
	local.sin_port = htons(port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = INADDR_ANY;
	
	if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
    
    
		std::cerr << "bind error" << std::endl;
		return 2;
	}

	// 3. 监听
	if (listen(listen_sock, num) < 0)
    {
    
    
		std::cerr << "listen error" << std::endl;
		return 3;
	}
	//启动服务器
	while(true)
    {
    
    
		//不调用accept获取连接
        sleep(1);
	}
	return 0;
}

Use thenetstat -natp | grep 进程名 command after running the server. You can see that the server is currently in the listening state.

Insert image description here

Next, create three new sessions. In all three sessions, we usetelnet to connect to our server program.

Insert image description here

Then we usenetstat -natp | head -2 && netstat -natp | grep 8081 to check the connection status.

Insert image description here

We found that the status of threetelnet client connections are allESTABLISH status, but for the server, there are only two< a i=3>state and astate. ESTABLISHSYN_RECV

Insert image description here

  • For the status ofSYN_RECV, it means that the server has not received the ACK from the client, but this is not the case here (the probability of packet loss is very small), for the client just now Even if the server receives the ACK from the client, the server cannot allow itself to enter the ESTABLISH state (the reason will be explained later).

  • Then if you use more clients to connect to the server later, you will find that only two clients can establish normal connections with the server, and the others will leave the server in the SYN_RCVD state, and This state is unstable. If the three-way handshake cannot be performed, the connection establishment will be abandoned.

2. TCP semi-connection queue and full-connection queue

In actual TCP connection management, there are two connection queues in the kernel:

  • Full connection queue (accept queue), the full connection queue is used to save the status that is already inESTABLISHED, but has not been called by the upper layer acceptThe connection taken away.
  • Semi-connection queue (SYN queue), the semi-connection queue is used to save the status of SYN_SENT and SYN_RECV Connection, that is, a connection that has not completed the three-way handshake.

The length of the full connection queue will actually be affected bylisten the second parameter. The length of the full connection queue is determined by two values:

  • The second parameter passed in when the user layer calls. listenbacklog
  • system change amountnet.core.somaxconn,默认值为128.

The length of the real full connection queue is equal to the smaller oflistenincomingbacklog and system variablenet.core.somaxconn Value plus1.

You can view the value of the system variablenet.core.somaxconn through the following command.

sudo sysctl -a | grep net.core.somaxconn

Insert image description here

Because we set the value of the second parameterlisten to 1 during the experiment, the length of the full connection queue on the server side is 2. Therefore, the server only allows a maximum of two connections in the ESTABLISHED state.

So when the third client sends a connection establishment request, the server will add a new status of SYN_RECV, and then put it into the semi-connection queue, even if the service The client received the ACK response from the client, but the full connection queue was full, so the third connection had no way to enter the full connection queue, so it remained in the SYN_RECV state.


You can use the ss command inlinux to view the status of the TCP full connection queue:

But it should be noted that the Recv-Q/Send-Q obtained by the ss command has different meanings in "LISTEN state" and "non-LISTEN state".

  • l Display the socket that is listening (listening)
  • n does not resolve service names
  • t shows only tcp sockets

In the "LISTEN state", the meaning of Recv-Q/Send-Q is as follows:
Insert image description here

  • Recv-Q: The size of the current full connection queue, that is, the TCP connection that has completed the three-way handshake and is waiting for the server accept();
  • Send-Q: The value is equal to the second parameter oflisten, indicating the current maximum queue length of the full connection - 1. The above output shows that the TCP service listening to port 8081 has the maximum full connection length of 2;

In the "non-LISTEN state", the meaning of Recv-Q/Send-Q is as follows:

Insert image description here

  • Recv-Q: The number of bytes received but not read by the application process;
  • Send-Q: The number of bytes sent but no confirmation received;

3. Some questions about the second parameter of listen

1. Question: Regardless of other parameters, what is the second parameter of listen so that the server can handle several connections at the same time?

The answer is:No. In our experiment we did not use the accept function, which led to the establishment of The connection cannot be taken away from the full connection queue. If we use the accept function and the connection is taken away, the server can process the connection. There are connections entering the full connection queue and connections being taken out. Go, the server can process all taken connections at the same time.

Therefore, this full connection queue is essentially a buffer, used to store connections that the operating system cannot handle for the time being. This model is also essentially a producer-consumer model.

  • Producer: semi-connected queue
  • Consumer: operating system
  • Buffer: full connection queue

2. Question: Why does the bottom layer maintain a connection queue?

Generally, the role of the connection queue will be reflected when the server pressure is high. If the server pressure itself is not high, then once a connection is successfully established at the bottom layer, the upper layer will immediately read the connection and process it.

When the server starts, it usually creates multiple service threads in advance to provide services for the client. After the main thread connects from the bottom layeraccept, it can be handed over to these service threads for processing. :

  • If there are very few clients that initiate connection requests to the server, then once the connection is established at the bottom layer, it will be immediately brought up by the main threadaccept and handed over to the service thread for processing.

  • However, if there are many clients initiating connection requests to the server, and when each service thread is providing services for a certain connection, the main thread will not be able to retrieve the connections established at the bottom layer. At this time, the connections that have been established on the bottom layer will It will be placed in the full connection queue. Only when a service thread is idle, the main thread will obtain the established connection from the full connection queue.

  • 如果没有这个连接队列,那么当服务器端的服务线程都在提供服务时,其他客户端发来的连接请求就会直接被拒绝,但有可能正当这个连接请求被拒绝时,某个服务线程提供服务完毕,此时这个服务线程就无法立马得到一个连接为之提供服务,所以一定有一段时间内这个服务线程是处于闲置状态的,直到再有客户端发来连接请求。

而如果设置了连接队列,当某个服务线程提供完服务后,如果连接队列当中有建立好的连接,那么主线程就可以立马从连接队列当中获取一个连接交给该服务线程进行处理,此时就可以保证服务器几乎是满载工作的。

3. 问:为什么连接队列既不能太长?

虽然维护连接队列能让服务器处于几乎满载工作的状态,但连接队列也不能设置得太长。

  • 第一:服务器维护连接也是需要成本的,连接队列设置的越长,系统就要花费越多的成本去维护这个队列。
  • 第二:如果队列太长,也就意味着在队列较尾部的连接需要等待较长时间才能得到服务,此时客户端的请求也就迟迟得不到响应。此外,与其维护一个很长的连接占用大量暂时用不到的资源,造成客户端等待过久,还不如将部分资源节省出来给服务器使用,让服务器更快的为客户端提供服务。

4、SYN洪水

Ⅰ、什么是SYN洪水攻击

我们都知道 TCP 连接建立是需要三次握手,假设攻击者用大量的假IP地址发送初始连接请求(SYN)数据包,让服务端建立连接,然后切换IP继续发,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

Ⅱ、如何解决SYN洪水攻击?

首先这一定是一个综合性的解决方案,TCP作为传输控制协议需要对其进行处理,而上层应用层也要尽量避免遭到SYN洪水攻击。

在应用层

  • 比如应用层可以记录向服务器发起连接建立请求的主机信息,如果发现某个主机多次向服务器发起SYN请求,但从不对服务器的SYN+ACK进行ACK响应,此时就可以对该主机进行黑名单认证,此后该主机发来的SYN请求一概不进行处理。

在传输层

现在核心的问题就是半连接队列被占满了,但不能简单的扩大半连接队列,就算半连接队列再大,恶意用户也能发送更多的SYN请求来占满,并且维护半连接队列当中的连接也是需要成本的。

于是TCP为了防范SYN洪水攻击,引入了syncookie机制:

开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接

syncookie的工作机制如下:

  • 当服务器收到一个SYN请求时,它会生成一个加密的SYN cookie,该cookie包含了客户端的IP地址、端口号和服务器使用的一些其他信息,并将cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;

  • 服务器不会保存任何关于该SYN连接的状态,即该链接不会进入半连接队列中,但是服务器会保存cookie的值。

  • 客户端收到SYN + ACK应答后,会解析其中的SYN cookie,并将其存储起来。

  • 客户端发送带有解析出的SYN cookie的ACK应答给服务器。

  • 服务器接收到ACK应答后,会验证ACK中的SYN cookie是否有效。如果有效,服务器会根据SYN cookie中的信息创建连接(进入全连接队列)并提供服务。

可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致半队列满时,也能保证正常的连接成功建立。

通过使用syncookie机制,服务器可以避免存储大量未完成的连接状态,从而抵御SYN洪水攻击。这种机制的优势是能够快速且有效地处理大量的SYN请求,并且不需要为每个半连接维护状态信息,减少了服务器的负担。

net.ipv4.tcp_syncookies 参数主要有以下三个值:

  • 0 值,表示关闭该功能;
  • 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
  • 2 值,表示无条件开启功能;

那么在应对 SYN 攻击时,只需要设置为 1 即可。

Insert image description here

Insert image description here

二、使用Wireshark分析TCP通信流程

wireshark是 windows 下的一个网络抓包工具. 虽然 Linux 命令行中有 tcpdump 工具同样能完成抓包,但是 tcpdump 是纯命令行界面,使用起来不如 wireshark 方便。

下载 wireshark

  1. 打开wireshark 2.6.5,主界面如下,选择对应的网卡(这里我们选择WLAN),点击即可进行捕获该网络信息,开始抓取网络包。
    Insert image description here

  2. 在使用Wireshark时可以通过设置过滤器,来抓取满足要求的数据包。


针对IP进行过滤

  • 抓取指定源地址的包:ip.src == 源IP地址
  • 抓取指定目的地址的包:ip.dst == 目的IP地址
  • 抓取源或目的地址满足要求的包:ip.addr == IP地址等价于ip.src ==源IP地址 or ip.dst == 目的IP地址
  • 抓取除指定IP地址之外的包:!(表达式)

针对协议进行过滤

  • 抓取指定协议的包:协议名(只能小写)
  • 抓取多种指定协议的包:协议名1 or协议名2
  • 抓取除指定协议之外的包:not 协议名 或 !协议名

针对端口进行过滤(以TCP协议为例):

  • 抓取指定端口的包:tcp.port == 端口号
  • 抓取多个指定端口的包:tcp.port >= 2048(抓取端口号高于2048的包)。

针对长度和内容进行过滤

  • 抓取指定长度的包:udp.length < 30 http.content_length <= 20
  • 抓取指定内容的包:http.request.urimatches "指定内容"

这里我们抓取指定源IP地址或目的IP地址的数据包,选择完毕记得按回车。

Insert image description here

  1. 在windows 的CMD使用 telnet 作为客户端连接上服务器 telnet [ip] [port]

Insert image description here

  1. 当我们用telnet命令连接该服务器后,就可以抓取到三次握手时双方交互的数据包。

Insert image description here

Insert image description here

  1. 而当我们退出telnet连接后,就可以抓取到四次挥手时双方交互的数据包。(此处四次挥手时进行了捎带应答,第二次挥手和第三次挥手合并在了一起,第二个包不是TCP数据包,是SSH数据包)
    Insert image description here

During the TCP waving process, if "there is no data to be sent" and "the TCP delayed acknowledgment mechanism is turned on", then the second and third waving will be combined and transmitted, resulting in three wavings.

Guess you like

Origin blog.csdn.net/qq_65207641/article/details/133801799