#TCP你學得會# 之 TCP_SYN_RECV的真相

    TCP的三次握手恐怕是網絡從業者最先接觸的幾個概念之一了(協議棧的分層架構應該是另外一個),可是一直以來你覺得的三次握手流程真的是你覺得的那樣嗎,此次咱們就重點關注一下TCP_SYN_RECV這個狀態,順藤摸瓜,看看它在Linux協議棧中的實現與預期是否相符。node

    TCP_SYN_RECV這個狀態是出如今三次握手流程中被動打開一方的,通常在現實中很難觀察到,由於它是由內核協議棧處理的,與應用程序無關,只要鏈接雙方的網絡暢通,這個狀態會稍縱即逝,若是咱們想要觀察到它,就須要藉助一些手段來實現了。網絡

握手流程中的最後一個ACK丟失啦   

    按照直觀的理解,TCP_SYN_RECV應該表明被動打開的一方(一般是server)接收到了來自client的SYN包,並使用SYN/ACK做爲迴應,下一步只要server端正確接收到來自client的最後一個確認包,三次握手就算大功告成了。那麼,第一個問題來了,若是client端的最後一個ACK丟失了會怎樣?
架構

    爲了模擬client端最後一個ACK丟失的情形而又不影響其它報文和流程,最方便的辦法是在server上經過iptables實現(server端的服務在54321端口監聽):
socket

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

    這樣發往54321端口的第一個SYN包可以正確經過,但最後一個ACK卻會被丟棄了(若是在--state中加上NEW狀態,則第一個SYN也會被丟棄)tcp

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模塊的處理流程):
測試

     SYN/ACK重傳超時前:ui

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重傳超時後:atom

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狀態。
spa

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。

    關於這一部分的內容尚未仔細研究過,就再也不指手畫腳了,歡迎指教。


    結論:你覺得你知道的,可能並無那麼知道。

相關文章
相關標籤/搜索