#TCP你学得会# 之 TCP_SYN_RECV的真相

    TCP的三次握手恐怕是网络从业者最早接触的几个概念之一了(协议栈的分层架构应该是另一个),但是一直以来你以为的三次握手流程真的是你以为的那样吗,这次我们就重点关注一下TCP_SYN_RECV这个状态,顺藤摸瓜,看看它在Linux协议栈中的实现与预期是否相符。

    TCP_SYN_RECV这个状态是出现在三次握手流程中被动打开一方的,一般在现实中很难观察到,因为它是由内核协议栈处理的,与应用程序无关,只要连接双方的网络畅通,这个状态会稍纵即逝,如果我们想要观察到它,就需要借助一些手段来实现了。

握手流程中的最后一个ACK丢失啦   

    按照直观的理解,TCP_SYN_RECV应该代表被动打开的一方(通常是server)接收到了来自client的SYN包,并使用SYN/ACK作为回应,下一步只要server端正确接收到来自client的最后一个确认包,三次握手就算大功告成了。那么,第一个问题来了,如果client端的最后一个ACK丢失了会怎样?

    为了模拟client端最后一个ACK丢失的情形而又不影响其它报文和流程,最方便的办法是在server上通过iptables实现(server端的服务在54321端口监听):

iptables -t filter -I INPUT -p tcp --dport 54321 -m state --state ESTABLISHED -j DROP

    这样发往54321端口的第一个SYN包能够正确通过,但最后一个ACK却会被丢弃了(如果在--state中加上NEW状态,则第一个SYN也会被丢弃)

1)再也等不到client发来的ACK

    通过抓包可以看到,server端由于一直没有收到最后一个ACK,会重传SYN/ACK,重传的次数由sysctl_tcp_synack_retries控制,默认是5。在重传过程中server端的netstat显示状态为SYN_RECV,而client端是ESTABLISHED,当达到重传次数的限制后,server端将放弃该连接,而client则对此一无所知,留下了一条半开连接。

    server端的抓包如下(注:No.3所显示的ACK是tcpdump抓到到,随后即被防火墙规则丢弃了,没有机会进入TCP模块的处理流程):

#TCP你学得会# 之 TCP_SYN_RECV的真相_第1张图片

     SYN/ACK重传超时前:

server:
tcp        0      0 10.237.101.81:54321     0.0.0.0:*               LISTEN      31983/tcp_server
tcp        0      0 10.237.101.81:54321     10.237.101.43:55972     SYN_RECV    - 

client:
tcp        0      0 192.168.28.67:55972     10.237.101.81:54321     ESTABLISHED -

    SYN/ACK重传超时后:

server:
tcp        0      0 10.237.101.81:54321     0.0.0.0:*               LISTEN      31983/tcp_server

client:
tcp        0      0 192.168.28.67:55972     10.237.101.81:54321     ESTABLISHED -  

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

2)client发送数据过来啦

    在上一个测试中client端在发起连接后什么也没做,但这种情况一般不会发生,client通常会在连接建立后立即开始发送数据。为了观察效果,我们在程序中控制client端在连接建立80s之后开始发送数据,并在此前将防火墙规则删除,以便client端的数据能够正常接收。如下图server端抓包所示,在SYN/ACK重传4次之后,我们将防火墙规则删除,client的数据也如期而至,这时,server端的连接能够正确转换至ESTABLISHED状态。

#TCP你学得会# 之 TCP_SYN_RECV的真相_第2张图片

client开始数据传输前:

server:
tcp        0      0 10.237.101.81:54321     0.0.0.0:*               LISTEN      396/tcp_server  
tcp        0      0 10.237.101.81:54321     10.237.101.43:55975     SYN_RECV    -  

client:
tcp        0      0 192.168.28.67:55975     10.237.101.81:54321     ESTABLISHED -

client开始传输数据后:

server:
tcp        0      0 10.237.101.81:54321     0.0.0.0:*               LISTEN      396/tcp_server  
tcp        0      0 10.237.101.81:54321     10.237.101.43:55975     ESTABLISHED 396/tcp_server 

client:
tcp        0      0 192.168.28.67:55975     10.237.101.81:54321     ESTABLISHED -

    所以说即使client端的最后一个ACK丢失了,只要它随后立即发送数据并顺利到达对端,server端依然能够正确转换至ESTABLISHED状态,这是因为数据报文中携带的ACK也能够起到确认的作用,这也是为什么在协议中没有对最后一个ACK设置超时等待的原因。

3)synack retransmit HOWTO

    顺着tcp_synack_retries的线索,再来找一找synack重传定时器的实现。

    sysctl_tcp_synack_retires是在inet_csk_reqsk_queue_prune()函数中调用的,而inet_csk_reqsk_queue_prune()又为tcp_synack_timer()所调用:

/*
 *	Timer for listening sockets
 */

static void tcp_synack_timer(struct sock *sk)
{
    inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL,
            TCP_TIMEOUT_INIT, TCP_RTO_MAX);
}

static void tcp_keepalive_timer (unsigned long data)
{
    ...
    if (sk->sk_state == TCP_LISTEN) {
	tcp_synack_timer(sk);
	goto out;
    }
    ...

resched:
    inet_csk_reset_keepalive_timer (sk, elapsed);
    goto out;
death:
    tcp_done(sk);
out:
    bh_unlock_sock(sk);
    sock_put(sk);
}

void tcp_init_xmit_timers(struct sock *sk)
{
    inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,
            &tcp_keepalive_timer);
}

    tcp_init_xmit_timers是在初始化一个socket(包括为一条新接收的连接创建socket)时调用的。可见,在初始化一个新的socket时会同时创建一个synack retransmit timer,并需要的时候负责SYN/ACK的重传工作。

TCP_SYN_RECV不是收到SYN?

    上面分析的一大段现在看起来好像没有任何意义,因为在我们的认识中它就应该是这个样子的,server收到SYN后会以SYN/ACK作为应答并进入TCP_SYN_RECV状态,合理又完美。但当我们想从协议栈源码中找到对应的依据时却傻眼了,因为根据协议栈的源码来分析的话,在收到SYN并应答SYN/ACK的处理流程中并没有设置连接为TCP_SYN_RECV状态这一步,而且事实上在这个流程中甚至并没有分配一个真正的struct sock结构,而只是分配了一个struct request_sock结构,那么netstat显示的SYN_RECV状态又该如何理解呢?

    难道是netstat显示时做了处理?

    netstat在读取TCP连接信息时实际上读取的是/proc/net/tcp这个文件,在tcp_ipv4.c中有对tcp procfs的注册及处理函数,netstat中显示的SYN_RECV状态实际是由该文件中的get_openreq4()函数处理的:

static void get_openreq4(const struct sock *sk, const struct request_sock *req,
			 struct seq_file *f, int i, int uid, int *len)
{
    const struct inet_request_sock *ireq = inet_rsk(req);
    int ttd = req->expires - jiffies;

    seq_printf(f, "%4d: %08X:%04X %08X:%04X"
	" %02X %08X:%08X %02X:%08lX %08X %5d %8d %u %d %pK%n",
	i,
	ireq->loc_addr,
	ntohs(inet_sk(sk)->inet_sport),
	ireq->rmt_addr,
	ntohs(ireq->rmt_port),
	TCP_SYN_RECV,
	0, 0, /* could print option size, but that is af dependent. */
	1,    /* timers active (only the expire timer) */
	jiffies_to_clock_t(ttd),
	req->retrans,
	uid,
	0,  /* non standard timer */
	0, /* open_requests have no inode */
	atomic_read(&sk->sk_refcnt),
	req,
	len);
}

static int tcp4_seq_show(struct seq_file *seq, void *v)
{
    struct tcp_iter_state *st;
    int len;

    if (v == SEQ_START_TOKEN) {
        seq_printf(seq, "%-*s\n", TMPSZ - 1,
            "  sl  local_address rem_address   st tx_queue "
            "rx_queue tr tm->when retrnsmt   uid  timeout "
            "inode");
	goto out;
    }
    st = seq->private;

    switch (st->state) {
        case TCP_SEQ_STATE_LISTENING:
	case TCP_SEQ_STATE_ESTABLISHED:
		get_tcp4_sock(v, seq, st->num, &len);
		break;
	case TCP_SEQ_STATE_OPENREQ:
		get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
		break;
	case TCP_SEQ_STATE_TIME_WAIT:
		get_timewait4_sock(v, seq, st->num, &len);
		break;
    }
    seq_printf(seq, "%*s\n", TMPSZ - 1 - len, "");
out:
    return 0;
}

    可以看到,对于TCP_SEQ_STATE_OPENREQ这个状态,会默认显示为SYN_RECV。

    关于这一部分的内容还没有仔细研究过,就不再指手画脚了,欢迎指教。


    结论:你以为你知道的,可能并没有那么知道。




你可能感兴趣的:(linux,TCP_SYN_RECV)