在排查网络问题的时候,经常会遇见TCP连接建立不成功的场景。如果能获取到两端抓包,两端抓包看起来如下:
- 客户端在一直按照指数退避重传TCP SYN (因为首包没有获取到RTT及RTO,会在1, 2, 4, 8秒…
重传,直到完成net.ipv4.tcp_syn_retries次重传) - 服务器端能看到TCP SYN报文已经到达网卡,但是TCP协议栈没有任何回包。
因为这样的问题出现的频率不小,本文会从TCP协议栈方面总结常见原因。所谓的TCP协议栈方面的原因,就是TCP SYN报文已经到了内核的TCP处理模块,但在服务器端内核逻辑中不给客户端回SYNACK。客户端一直重传TCP SYN也可能由别的原因造成,比如服务器端有多块网卡造成的出入路径不一致,或者SYN报文被iptables规则阻拦,这些场景都不在本文的讨论范围之内。
Listen状态下处理TCP SYN的代码逻辑
本文以很多用户使用的CentOS 7的内核版本为基础,看看下TCP处理SYN的主要逻辑,结合案例处理的经验来分析主要可能出问题的点。处于listen状态的socket处理第一个TCP SYN报文的逻辑大概如下:
tcp_v4_do_rcv() @net/ipv4/tcp_ipv4.c
|--> tcp_rcv_state_process() @net/ipv4/tcp_input.c // 这个函数实现了绝大TCP状态下的接受报文的处理过程 (ESTABLISHED和TIME_WAIT除外),当然包括了我们关注的LISTEN状态
|--> tcp_v4_conn_request() @@net/ipv4/tcp_ipv4.c // 当TCP socket出于LISTEN状态,且接收报文中TCP SYN flag是置位的,就来到这个函数中处理
CentOS中内核代码可能会有些调整,如果你需要跟踪源代码的确切行数,systemtap是一个很好的方法,如下:
# uname -r
3.10.0-693.2.2.el7.x86_64
# stap -l 'kernel.function("tcp_v4_conn_request")'
kernel.function("tcp_v4_conn_request@net/ipv4/tcp_ipv4.c:1303")
来到tcp_v4_conn_request()的逻辑里,函数逻辑的前几行如下:
进入到这个函数的前提条件是TCP socket出于LISTEN状态,且接收报文中TCP SYN flag是置位的。在进入函数逻辑后,可以发现函数要考虑各种可能发生的异常情况,但在现实中很多并不常见。比如我们在前几行看到的这两种情况:
- 1482行:拒绝广播和组播报文。
- 1490行:如果request queue (存放SYN报文的队列)满了,且isn为0,且want_cookie为flase,
则drop掉SYN报文。
第一种情况意思比较明确,在实际中也没见过,在这里不讨论。第二种情况略为复杂,并且有小概率可能会碰到,下面简单看看:
第一个条件request queue 满实际是很容易发生的事情,syn flood攻击很容易完成这件事情。而isn在函数开始被赋值成TCP_SKB_CB(skb)->when,这个是TCP控制块结构体中用于计算RTT的字段。want_cookie则代表这syn syncookies的使用与否。在tcp_syn_flood_action()中的定义如下,如果ifdef了CONFIG_SYN_COOKIES, 内核参数的net.ipv4.tcp_syncookies也设置成1,则概述的返回是true, want_cookie则为true。
所以在上面这种drop SYN报文的情况中,真正的前提条件是没有开启net.ipv4.tcp_syncookies这个内核参数。而在实际生产系统中,net.ipv4.tcp_syncookies默认是打开的。Syn syncookies是一种时间(CPU计算)换空间(request queue队列)来抵御syn flood攻击的方式,在实际生产中看不到任何场景需要显示地关闭这个开关。所以总的来讲,1490行中这种请求在实际中也不太常见。
内核drop SYN报文的主要场景
本文的主要目的不是按照代码逻辑依次描述drop SYN报文的所有场景,而是结合之前的实际经验描述两种主要可能丢SYN报文的场景以及如何迅速判断的方