TCP建立连接时socket的epoll态及一个可能的状态不一致问题

零、原因
其实本来是在看TCP三次握手时客户端和服务器端socket对于epoll状态何时返回何种状态,不过后来引出了一个另有意思的问题:就是客户端和服务器双方对于三次握手的状态出现了不一致。我们知道,在三次握手中,客户端在发送最后一个ack之后进入ESTABLISHED状态,并没有要求服务器对于这个ACK再次ACK(当然也没有办法要求ACK,否则这样就是没完没了的ACK了),所以通常我们认为ACK是对于数据的ACK,而ACK本身并不需要被再次ACK。这个一致性问题可以用下面的图片来表示,可以将其中的男吊看作是客户端,它认为自己进入的ESTABLISHED状态,而女神可以认为是服务器,其实这个连接在服务器端并没有建立。
TCP建立连接时socket的epoll态及一个可能的状态不一致问题 - Tsecer - Tsecer的回音岛
 
一、listen方对于状态的判断
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
……
if (sk->sk_state == TCP_LISTEN)
return inet_csk_listen_poll(sk);
可以看到对于服务器端正在侦听的套接口,对于侦听状态做了特殊处理,而没有执行下面通用的poll状态判断
 
/*
 * LISTEN is a special case for poll..
 */
static inline unsigned int inet_csk_listen_poll(const struct sock *sk)
{
return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ?
(POLLIN | POLLRDNORM) : 0;
}
二、侦听套接口icsk_accept_queue队列的填充
tcp_v4_hnd_req==>>tcp_check_req==>>inet_csk_reqsk_queue_add
static inline void inet_csk_reqsk_queue_add(struct sock *sk,
    struct request_sock *req,
    struct sock *child)
{
reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child);
}
在这个路径中,其中tcp_check_req函数的注释中说明了这个处理的是SYN_RECV状态下的socket(Process an incoming packet for SYN_RECV sockets represented as a request_sock),也就是三次握手中最后一次交互。那么三次握手中的第一个包的处理流程呢?
三、服务器端对于第一次握手报文的处理
tcp_v4_do_rcv==>>tcp_rcv_state_process==>>tcp_v4_conn_request
/* TW buckets are converted to open requests without
 * limitations, they conserve resources and peer is
 * evidently real one.
 */
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
 
/* Accept backlog is full. If we have already queued enough
 * of warm entries in syn queue, drop request. It is better than
 * clogging syn queue with openreqs with exponentially increasing
 * timeout.
 */
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
 ……
 drop:
TCP_INC_STATS_BH(TCP_MIB_ATTEMPTFAILS);
return 0;
}
在tcp_rcv_state_process函数中
case TCP_LISTEN:
if(th->ack)
return 1;
 
if(th->rst)
goto discard;
 
if(th->syn) {
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
 
/* Now we have several options: In theory there is 
 * nothing else in the frame. KA9Q has an option to 
 * send data with the syn, BSD accepts data with the
 * syn up to the [to be] advertised window and 
 * Solaris 2.1 gives you a protocol error. For now 
 * we just ignore it, that fits the spec precisely 
 * and avoids incompatibilities. It would be nice in
 * future to drop through and process the data.
 *
 * Now that TTCP is starting to be used we ought to 
 * queue this data.
 * But, this leaves one open to an easy denial of
   * service attack, and SYN cookies can't defend
 * against this problem. So, we drop the data
 * in the interest of security over speed.
 */
goto discard;
}
goto discard;
当超过限量之后,这个地方就会出现报文被丢弃,此时也就是意味着客户端的首次握手报文不会收到回包,注意,这是一次非常不友好的第一印象。
四、服务器端对于连接数的处理
我们再回过头来看下tcp_v4_conn_request函数对于首次握手的处理,在这个函数中分别进行了两次判断,一个是inet_csk_reqsk_queue_is_full,它使用的是icsk_accept_queue队列是否为空;然后是sk_acceptq_is_full,它使用的是sk_ack_backlog。这里的两个队列可以大致认为icsk_accept_queue是只收到了第一次握手的socket,这里的accept其实只是越过了listen socket的第一次检测,说明服务器“有意向”(而不是拒绝)接受客户端的三次握手,三次握手的流程可以继续,但是并没有完成。而sk_ack_backlog队列则是包含了已经完成了握手,但是服务器端的守护程序并没有通过accept将这个已经完成三次握手的连接接收过去,所以此时是可用但是用户态程序没有使用,所以暂存在了listen socket的sk_ack_backlog队列中。
在判断backlog队列sk_acceptq_is_full时,有一个额外的判断条件inet_csk_reqsk_queue_young(sk) > 1,这里的young是指ack报文没有被重传过的socket。而inet_csk_reqsk_queue_young会在tcp_synack_timer==>>inet_csk_reqsk_queue_prune中定时清除,所以理论上来说,一个socket可以接收的最多连接数量是acceptq和backlogqueue的长度之和。但是,当三次握手真正完成之后tcp_v4_syn_recv_sock函数的开始就会判断这个backlog队列是否已经满了,如果满了之后就直接丢弃连接,也就是功亏一篑,三次握手在最后一次我手上收到之后并没有建立,所以此时客户端就需要再次(多次重传)。
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
  struct request_sock *req,
  struct dst_entry *dst)
{
……
if (sk_acceptq_is_full(sk))
goto exit_overflow;
五、backlog队列在什么时候会满
看了这么多,这个backlog队列的长度是连接是否可以建立的关键。
static inline int sk_acceptq_is_full(struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
这个值就是listen系统调用的第二个参数,这是一个严格的用户传入数值,也就是最多有这么多个完成了三次握手但是并没有被用户进程accept的socket。和这个对应的是可以完成一次握手socket的数量,这个在inet_csk_listen_start===>>>reqsk_queue_alloc创建,
在3.12.6内核版本中,这个真正生效的上限值由reqsk_queue_alloc函数中的下面代码控制,其中传入的原始nr_table_entries也就是listen中的backlog数值。
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
        ……
for (lopt->max_qlen_log = 3;
     (1 << lopt->max_qlen_log) < nr_table_entries;
     lopt->max_qlen_log++);
     也就是说,这个可以同时接受的首次握手请求通常是用户设置值向上取整之后的数量。
六、客户端的connect何时返回
inet_stream_connect==>>inet_wait_for_connect
while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
release_sock(sk);
timeo = schedule_timeout(timeo);
lock_sock(sk);
if (signal_pending(current) || !timeo)
break;
prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);
}
这意味着在没有收到服务器的ACK之前,connect将会一直阻塞在connect上(状态始终为TCPF_SYN_SENT),所以客户端有可能在connect时产生阻塞,对于异步框架来说,在connect的时候最好非阻塞来连接服务器,然后通过poll来查询状态,在前面的代码中可以看到,tcp_poll直接排除了SYN_SENT和SYN_RECV两个状态对结果的影响,它们不会生成有效的poll输出结果:
if ((1 << sk->sk_state) & ~(TCPF_SYN_SENT | TCPF_SYN_RECV)) {
……
}
return mask;
七、客户端对于connect重传的处理
当客户端的connect没有收到服务器回包时,此时会发生超时,进入tcp_write_timeout函数,这里对于同步连接的时候使用了特殊的配置值,也就是sysctl_tcp_syn_retries变量作为重试次数的上限,默认值为5次。
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
if (icsk->icsk_retransmits)
dst_negative_advice(&sk->sk_dst_cache);
retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
}
这意味这一个问题,如果服务器发生拥塞,长时间不accept操作系统已经完成三次握手的backlog连接,后来的客户端connect的时候将会在阻塞的情况下进行重传。   
这个重传时间以     TCP_TIMEOUT_INIT,也就是3秒为基准,按照倍增方法增加,
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */
尝试次数为sysctl_tcp_syn_retries,其中该值默认为5
#define TCP_SYN_RETRIES  5 /* number of times to retry active opening a
 * connection: ~180sec is RFC minimum */
所以在客户端第5次尝试超时之后认为连接失败,所以假设初始超时时间为1s,在第5次超时之后,总共尝试时间为大致为2^6=64s。奇怪的是好像不同版本中TCP_TIMEOUT_INIT这个值不同,早期版本初始值为3,新的版本为1。
八、服务器端accept何时返回      
1、阻塞态下accept
inet_accept===>>inet_csk_accept===>>inet_csk_wait_for_connect
for (;;) {
                ……
if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
break;
                ……
 }                   
2、poll
tcp_poll===>>inet_csk_listen_poll
/*
 * LISTEN is a special case for poll..
 */
static inline unsigned int inet_csk_listen_poll(const struct sock *sk)
{
return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ?
(POLLIN | POLLRDNORM) : 0;
}
根据前面的分析,之后完成了三次握手的连接才会被放入到request_sock_queue::rskq_accept_head队列中,所以只完成了一次握手的连接不会将accept唤醒。这个直观上理解是自然而然的,不过还是看到这个代码实现更踏实些。
3、reqsk_queue_empty
函数实现为
static inline int reqsk_queue_empty(struct request_sock_queue *queue)
{
return queue->rskq_accept_head == NULL;
}
这个地方使用的并不是request_sock_queue::listen_sock::qlen,而是判断了queue->rskq_accept_head 队列是否为空。再次强调request_sock_queue::listen_sock::qlen表示已经接受的首次握手的连接,它们保存在listen_sock::syn_table[]数组中。
九、服务器端backlog拥塞之后
1、测试代码
非常简单的逻辑,就是服务器值listen,但是不执行任何的accept操作,并且设置listen的backlog参数为1,客户端同时发起多个连接,试图将服务器中的backlog耗光。
tsecer@harry: cat svr.cpp 
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <stdlib.h>
       #include <stdio.h>
       #include <string.h>
      #include <sys/types.h>
       #include <sys/socket.h>
       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>
 
 
       #define handle_error(msg) \
           do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
       int
       main(int argc, char *argv[])
       {
           struct addrinfo hints;
           struct addrinfo *result, *rp;
           int sfd, s, j;
           size_t len;
           ssize_t nread;
        sockaddr_in stpeer;
 
           memset(&hints, 0, sizeof(struct addrinfo));
           hints.ai_family = AF_INET;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_STREAM; /* Datagram socket */
           hints.ai_flags = 0;
           hints.ai_protocol = 0;          /* Any protocol */
 
           s = getaddrinfo(argv[1], argv[2], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }
           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;
           if (bind(sfd, (struct sockaddr *) rp->ai_addr,
                   rp->ai_addrlen) == -1)
               handle_error("bind");
 
           if (listen(sfd, 1) == -1)
               handle_error("listen");
 
sleep(100000);
}
       }
tsecer@harry: cat cli.cpp 
       #include <sys/types.h>
       #include <sys/socket.h>
       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>
 
       #define BUF_SIZE 500
 
       int
       main(int argc, char *argv[])
       {
           struct addrinfo hints;
           struct addrinfo *result, *rp;
           int sfd, s, j;
           size_t len;
           ssize_t nread;
          char buf[BUF_SIZE];
 
           if (argc < 3) {
               fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
               exit(EXIT_FAILURE);
           }
 
           /* Obtain address(es) matching host/port */
 
           memset(&hints, 0, sizeof(struct addrinfo));
           hints.ai_family = AF_INET;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_STREAM; /* Datagram socket */
           hints.ai_flags = 0;
           hints.ai_protocol = 0;          /* Any protocol */
 
           s = getaddrinfo(argv[1], argv[2], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }
           for (rp = result; rp != NULL; rp = rp->ai_next) {
int itry = atoi(argv[3]);
for (int t = 0; t < itry ; t++)
{
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;
               if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
                {
printf("connection %d\n", t);
                }
else
{
perror("connect failed");
}
}
           }
sleep(10000);
}           
2、原始测试
[root@localhost listenfull]# export PS1="tsecer@harry: "
tsecer@harry: ./svr 127.0.0.1 1234
tsecer@harry: ./cli 127.0.0.1 1234 10
connection 0
connection 1
connection 2
connection 3
connection 4
connection 5
connection 6
connection 7
connection 8
connection 9
客户端认为自己成功的完成了所有的10次握手,全部进入ESTABLISHED状态。通过netstat可以看到,svr只有两个连接进入了ESTABLISHED(第四列为127.0.0.1:1234,第六列为ESTABLISHED的列),而cli则有10项进入ESTABLISHED,此时客户端和服务器的状态已经出现不一致:客户端认为三次握手已经完成,而服务器认为还没有
tsecer@harry: netstat -anp | grep 1234
tcp        2      0 127.0.0.1:1234          0.0.0.0:*               LISTEN      10796/./svr         
tcp        0      0 127.0.0.1:1234          127.0.0.1:44839         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44838         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44841         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44842         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44836         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44837         SYN_RECV    -                   
tcp        0      0 127.0.0.1:1234          127.0.0.1:44840         SYN_RECV    -                   
tcp        0      0 127.0.0.1:44835         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:1234          127.0.0.1:44833         ESTABLISHED -                   
tcp        0      0 127.0.0.1:44841         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44840         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44837         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44833         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44839         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44834         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:1234          127.0.0.1:44834         ESTABLISHED -                   
tcp        0      0 127.0.0.1:44838         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44836         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tcp        0      0 127.0.0.1:44842         127.0.0.1:1234          ESTABLISHED 10800/./cli         
tsecer@harry: 
3、为什么可以绕过tcp_v4_conn_request的检测
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
 
if ((sysctl_tcp_syncookies == 2 ||
     inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
if (!want_cookie)
goto drop;
}
如果配置了tcp_syncookies,那么请求队列满的限制将不会生效,对于后一个判断
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
当服务器对首次握手的syn发送ack之后会启动保活定时器,这个定时的执行函数在tcp_synack_timer===>>>inet_csk_reqsk_queue_prune,该定时器每隔一段时间(#define TCP_SYNQ_INTERVAL (HZ/5) /* Period of SYNACK timer */)启动一次,对于未三次握手的半连接进行清除,所以随着系统时间的推进,定时器清空了request_sock_queue.listen_opt队列中的半连接,并且将inet_csk_reqsk_queue_young清除,所以之后客户端重传过来的syn请求会被服务区逐渐接受。从效果上看,就是我们看到客户端陆续完成了任意多次连接,但是通过netstat看服务器端并没有。
4、如何避免这种情况
在三次握手的最后一次确认中,
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
   struct request_sock *req,
   struct request_sock **prev,
   bool fastopen)
……
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;                       
……
listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}
 
embryonic_reset:
if (!(flg & TCP_FLAG_RST)) {
/* Received a bad SYN pkt - for TFO We try not to reset
 * the local connection unless it's really necessary to
 * avoid becoming vulnerable to outside attack aiming at
 * resetting legit local connections.
 */
req->rsk_ops->send_reset(sk, skb);
}
其中syn_recv_sock调用的是                           
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
  struct request_sock *req,
  struct dst_entry *dst)
if (sk_acceptq_is_full(sk))
goto exit_overflow;
……
exit_overflow:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
exit_nonewsk:
dst_release(dst);
exit:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
return NULL;
所以设置了sysctl_tcp_abort_on_overflow标志位之后,则之后的连接会被reset掉。
tsecer@harry: cat /proc/sys/net/ipv4/tcp_abort_on_overflow 
0
tsecer@harry: echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow 
tsecer@harry: ./cli 127.0.0.1 1234 10 
connection 0
connection 1
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer
connect: Connection reset by peer

posted on 2019-03-07 09:57  tsecer  阅读(861)  评论(0编辑  收藏  举报

导航