Last updated
Last updated
在基于 TCP 的服务开发中,三次握手的主要流程如下:
客户端:发送 SYN 包
服务端:回复 ACK
客户端:回复 ACK
本篇来看一下三次握手的具体实现(基于 Linux3.0 源码)
客户端程序的核心代码如下:
服务端的如下:
listen
函数内部,主要做了半连接队列和全连接队列的初始化、内存申请过程。
半连接队列用于存放未完成三次握手的连接(刚从客户端发送过来的 SYN包
全连接队列用于存放完成三次握手的连接
这里先随机选一个端口,再发送 SYN 报文
端口的选择
发送 SYN包
conn_request
指向 tcp_v4_conn_request
函数,是服务端响应 SYN 的主要逻辑函数
SYN Flood攻击
一般情况下,半连接的"生存"时间其实很短,只有在第一次和第三次握手间,如果半连接都满了,说明服务端疯狂收到第一次握手请求。如果是线上游戏应用,能有这么多请求进来,那说明你可能要富了。但现实往往比较骨感,你可能遇到了SYN Flood攻击。
所谓SYN Flood攻击,可以简单理解为,攻击方模拟客户端疯狂发第一次握手请求过来,在服务端憨憨地回复第二次握手过去之后,客户端死活不发第三次握手过来,这样做,可以把服务端半连接队列打满,从而导致正常连接不能正常进来。
那这种情况怎么处理?有没有一种方法可以绕过半连接队列?
有,上面的代码里可以看到一个 tcp_syn_flood_action
函数,里面读取的是内核的 proc/sys/net/ipv4/tcp_syncookies
参数。
当它被设置为1的时候,客户端发来第一次握手SYN时,服务端不会将其放入半连接队列中,而是通过通信双方的IP地址端口、时间戳、MSS等信息实时计算一个 cookies
,保存在TCP报头的 seq
里。这个 cookies
会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies
,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。
cookies方案为什么不直接取代半连接队列
cookies
方案虽然能防 SYN Flood攻击,但是也有一些问题。因为服务端并不会保存连接信息,所以如果传输过程中数据包丢了,也不会重发第二次握手的信息。
另外,编码解码cookies
,都是比较耗CPU的,利用这一点,如果此时攻击者构造大量的第三次握手包(ACK包),同时带上各种瞎编的cookies
信息,服务端收到ACK包
后以为是正经cookies,憨憨地跑去解码(耗CPU),最后发现不是正经数据包后才丢弃。
这种通过构造大量 ACK包
去消耗服务端资源的攻击,叫ACK攻击,受到攻击的服务器可能会因为CPU资源耗尽导致没能响应正经请求。
客户端也会进入到 tcp_rcv_state_process
函数中。不过由于自身 socket 的状态是 TCP_SYN_SENT
,所以会进入到另一个不同的分支中去。
就是在这一步客户端修改了自己的 socket 状态为 ESTABLISHED
,并清除了 connect 时设置的重传定时器,开启了保活计时器
服务器响应第三次握手的 ack 时同样会进入到第二次握手时的 tcp_v4_do_rcv
函数,不过之前在半连接队列里找不到连接,这次可以了,因此会进入不同的条件分支 tcp_v4_hnd_req
。
这里可以看到,半连接队列并不是用队列实现的,而是用哈希表,根据地址、端口等参数计算了个哈希函数。这样就可以 O(1) 时间查找到半连接。
最后 accept 一步就是从全连接队列里取个连接出来处理。和半连接队列用 hashmap 实现不同,这个队列则是用链表实现的。因为服务端此时并不关心具体是哪个连接,直接从队列头取一个出来处理就行了。
服务器 listen,计算全/半连接队列的长度,申请相关内存并初始化。
客户端 connect 时,把本地 socket 状态设置成了 TCP_SYN_SENT,选择一个可用的端口,发出 SYN 握手请求并启动重传定时器。
服务器响应 ack 时,会判断下接收队列是否满了,满的话可能会丢弃该请求。否则发出 synack,申请将 request_sock 添加到半连接队列中,同时启动定时器。
客户端响应 synack 时,清除了 connect 时设置的重传定时器,把当前 socket 状态设置为 ESTABLISHED,开启保活计时器后发出第三次握手的 ack 确认。
服务器响应 ack 时,把对应半连接对象删除,创建了新的 sock 后加入到全连接队列中,最后将新连接状态设置为 ESTABLISHED。
accept 从已经建立好的全连接队列中取出一个返回给用户进程。
另外要注意的是,如果握手过程中发生丢包(网络问题,或者是连接队列溢出),内核会等待定时器到期后重试,重试时间间隔在 3.10 版本里分别是 1s 2s 4s ...。在一些老版本里,比如 2.6 里,第一次重试时间是 3 秒。最大重试次数分别由 tcp_syn_retries 和 tcp_synack_retries 控制。
如果你的线上接口正常都是几十毫秒内返回,但偶尔出现了 1 s、或者 3 s 等这种偶发的响应耗时变长的问题,那么你就要去定位一下看看是不是出现了握手包的超时重传了。
参考