A Tencent interview question: Can a TCP connection be established without listen

The biggest difference between TCP and UDP is the concept of connection, and the establishment of the connection is done by the kernel. The system calls listen to tell the kernel that it needs to process the connection request sent to this TCP port. Therefore, the most direct idea for this topic is that the application layer is responsible for the TCP connection. In order to be able to receive TCP handshake packets, you can try to use the original socket to receive IP packets, so that you can replace the kernel in the application layer to do the TCP three-way handshake. This idea is good, but the reality is cruel.

When the TCP socket is not in the listen state and the raw socket is used to process the handshake message, even if the syn message is received and the syn+ack message is sent to the peer, the connection cannot be completed. Because the kernel generally sends an RST in advance to interrupt the connection. Seven years ago, I only knew this result. Now, you can clearly know why the kernel sends an RST packet to interrupt the connection. The kernel first copies the message to the original socket in ip_local_deliver_finish, and then continues the subsequent processing and enters the tcp receiving function tcp_v4_rcv. In this function, a socket search is required.
Insert picture description here

Because there is no monitoring tcp socket, it is naturally unable to find the corresponding socket. So jump to no_tcp_socket.
Insert picture description here

In this error handling, as long as the checksum of the packet skb is correct, the kernel will call tcp_v4_send_reset to send an RST to terminate the connection. Therefore, this solution using raw socket alone will not work. We need to find a way to prohibit the kernel from processing the TCP message. At this time, you can use the NFQUEUE of iptables to fetch the data packet to the application layer at the network layer, and reply syn+ack, and at the same time inform the kernel that the data packet has been "stolen" during reinject, that is, NF_STOLEN. In this way, the kernel will not continue to process the packet skb. Although I did not continue to experiment at the time, in theory, through this IPtables NFQUEUE+NFSTOLEN solution, it is possible to achieve "no listen, establish a TCP connection".

[Article benefits] Interview questions from major Internet companies and C/C++ Linux server architect learning materials plus group 812855908 (data including C/C++, Linux, golang technology, Nginx, ZeroMQ, MySQL, Redis, fastdfs, MongoDB, ZK , Streaming media, CDN, P2P, K8S, Docker, TCP/IP, coroutine, DPDK, ffmpeg, etc.)
Insert picture description here

Insert picture description here

Unfortunately, in the discussion with that student, the original intention of Tencent's interview question was not this, but for ordinary TCP sockets, if there is no listen call, whether it is possible to create a connection. Even if the conditions are limited, the answer is still yes. After defining the conditions, we need to determine two things:

  1. Similar to the previous one, how to avoid the kernel from sending RST. Under the premise that iptable cannot be used, this means that the corresponding socket must be found in tcp_v4_rcv.
  2. Without a socket in the listen state, can the kernel complete the TCP three-way handshake?

It is relatively simple to determine the first question. You only need to think deeply about the three-way handshake to get the answer. In the normal three-way handshake, when the server replies with syn+ack, the client does not actually have a socket in the listen state, but it can complete the three-way handshake. This means that after the client makes a connect call, the socket must be added to a table and can be matched. Tracing the kernel source code tcp_v4_connect->inet_hash_connect->__inet_check_established, you can see that when connect is called, the corresponding socket is added to the global tcp connected table, that is, tcp_hashinfo.ehash. The corresponding matching TCP socket process is as follows __inet_lookup_skb->__inet_lookup
Insert picture description here

The kernel searches in the connected table first, and then searches the listen table. For the client, the syn+ack message must match the corresponding socket in the connected table. So, for this topic, if you want both ends to be able to find the socket, you need to call connect on both ends before the message arrives. In other words, when both ends call connect at the same time, the syn packets on both ends can match the local socket.

Next, it is only necessary to determine whether the syn packet can be processed normally for the client socket. The tcp_rcv_synsent_state_process function is a processing function when the TCP socket is in the synsent state (that is, connect is called). The following is part of its implementation code:
Insert picture description here
Insert picture description here

From the above, it can be concluded that for a socket in the synsent state, if a syn message is received, it will reply synack normally and complete the TCP three-way handshake.

For this interview question of Tencent, the answer is that when both ends initiate a connect call at the same time, even if there is no listen call, the TCP connection can be successfully established.

If the restriction of "both ends" is removed, another answer is that a TCP socket can connect to the address and port of its own bind, and it can also meet the requirements. The following is the test code, which realizes a TCP socket to successfully connect to itself and send a message.

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define LOCAL_IP_ADDR		(0x7F000001)
#define LOCAL_TCP_PORT		(34567)

int main(void)
{
    
    
	struct sockaddr_in local, peer;
	int ret;
	char buf[128];
	int sock = socket(AF_INET, SOCK_STREAM, 0);

	memset(&local, 0, sizeof(local));
	memset(&peer, 0, sizeof(peer));

	local.sin_family = AF_INET;
	local.sin_port = htons(LOCAL_TCP_PORT);
	local.sin_addr.s_addr = htonl(LOCAL_IP_ADDR);

	peer = local;	

    int flag = 1;
    ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    if (ret == -1) {
    
    
        printf("Fail to setsocket SO_REUSEADDR: %s\n", strerror(errno));
        exit(1);
    }

	ret = bind(sock, (const struct sockaddr *)&local, sizeof(local));
	ret = connect(sock, (const struct sockaddr *)&peer, sizeof(peer));

	if (ret) {
    
    
		printf("Fail to connect myself: %s\n", strerror(errno));
		exit(1);
	}
	printf("Connect to myself successfully\n");

	strcpy(buf, "Hello, myself~");
	send(sock, buf, strlen(buf), 0);

	memset(buf, 0, sizeof(buf));
	recv(sock, buf, sizeof(buf), 0);

	printf("Recv the msg: %s\n", buf);
	close(sock);
	return 0;
}

The connection screenshot is as follows:
Insert picture description here

From the screenshot, you can see that the TCP socket successfully "connected" itself, and is surrounded by data sent and received. The output of netstat proves that the addresses and ports of both ends of TCP are exactly the same.

Guess you like

Origin blog.csdn.net/qq_40989769/article/details/110875312