TCP的三次握手恐怕是网络从业者最早接触的几个概念之一了(协议栈的分层架构应该是另一个),但是一直以来你以为的三次握手流程真的是你以为的那样吗,这次我们就重点关注一下TCP_SYN_RECV这个状态,顺藤摸瓜,看看它在Linux协议栈中的实现与预期是否相符。
TCP_SYN_RECV这个状态是出现在三次握手流程中被动打开一方的,一般在现实中很难观察到,因为它是由内核协议栈处理的,与应用程序无关,只要连接双方的网络畅通,这个状态会稍纵即逝,如果我们想要观察到它,就需要借助一些手段来实现了。
按照直观的理解,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也会被丢弃)
通过抓包可以看到,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重传超时前:
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
在上一个测试中client端在发起连接后什么也没做,但这种情况一般不会发生,client通常会在连接建立后立即开始发送数据。为了观察效果,我们在程序中控制client端在连接建立80s之后开始发送数据,并在此前将防火墙规则删除,以便client端的数据能够正常接收。如下图server端抓包所示,在SYN/ACK重传4次之后,我们将防火墙规则删除,client的数据也如期而至,这时,server端的连接能够正确转换至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 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设置超时等待的原因。
顺着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的重传工作。
上面分析的一大段现在看起来好像没有任何意义,因为在我们的认识中它就应该是这个样子的,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。
关于这一部分的内容还没有仔细研究过,就不再指手画脚了,欢迎指教。
结论:你以为你知道的,可能并没有那么知道。