一、TCP报文格式
TCP报文格式图:
上图中有几个字段介绍下:
(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。
二、三次握手的机制与过程
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
假设 client 的连接请求延迟,client 就会发出第二次连接请求,server 端回复后则建立了连接,进行通信。等到通信结束,连接关闭,此时有可能第一次的连接请求才到 server 端,那么此时server 回复报文之后,就认为连接已建立,但在 client 端看来, 根本没有发起连接请求(连接建立重试后,已完成通信并关闭连接),所以会忽略 server 端的报文,也不会向这个链接发送数据。此时对于 server 端来说,就会因为维护这个链接而导致资源浪费。
三次握手的详细过程如下:
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
关于SYN攻击:
在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
#netstat -nap | grep SYN_RECV
整个TCP状态图示意如下:
三、 Linux中TCP握手过程实现
在Linux应用编程如果设置为非阻塞模式,则连接时,connect发送SYN包后立即返回-EINPROGRESS,表示操作正在处理中;随后应用可以在connect返回后做一些其它的处理,最后在select函数中来捕获socket的连接、读写、异常事件以触发相关操作,下面我们看看内核中的相关实现:
一、客户端支持
client发送2个包,一个SYN包,一个对服务器的响应ACK包。
client函数调用链:connect-->sys_connect->inet_stream_connect->tcp_connect...
看inet_stream_connect中实现的部分代码段:
switch (sock->state) { ... /*此处调用tcp_connect函数发送SYN包*/ err = sk->prot->connect(sk, uaddr, addr_len); if (err < 0) //出错则退出 goto out; sock->state = SS_CONNECTING; /* 此处仅设置socket的状态为SS_CONNECTING表示连接状态正在处理; * 不同之处在于非阻塞情况下,返回值设置为-EINPROGRESS表示操作正在处理 * 而阻塞式情况则在获得ACK包后将返回值置为-EALREADY. */ err = -EINPROGRESS; break; } timeo = sock_sndtimeo(sk, flags&O_NONBLOCK); //注意,如果此时设置了非阻塞选项,则timeo返回0 //如果socket对应的sock状态是SYN包已发送或收到SYN包并发送了ACK包,并等待对端发送第三此的ACK包 if ((1<state)&(TCPF_SYN_SENT|TCPF_SYN_RECV)) { /* 错误返回码err前面已经设置 */ if (!timeo || !inet_wait_for_connect(sk, timeo)) /*注意上面所判断的2中情况,1、如果是非阻塞模式,则!timeo为1,则直接跳到out返回-EINPROGRESS结束connect函数 2、若为阻塞模式,则在inet_wait_for_connect函数中通过schedule_timeout函数放弃cpu控制权睡眠,等待服务器端 发送ACK响应包后被唤醒继续处理。如果没有异常出现,则置socket状态为SS_CONNECTED,表示连接成功,正确返回 */ goto out; err = sock_intr_errno(timeo); if (signal_pending(current)) /*处理未决信号*/ goto out; } ... sock->state = SS_CONNECTED; err = 0; out: release_sock(sk); return err;
上面的描述有一个问题:对服务器的响应ACK包是什么时候发送的?对于非阻塞模式,应该是应用处理过程中的某个异步时间;对于阻塞模式,则是在inet_wait_for_connect函数中睡眠时处理。即网卡在收到对方的ack包后,上传给对应的socket时发送服务器的响应ACK包,函数调用链为:netif_rx-->net_rx_action-->...(IP层处理)-->tcp_v4_rcv-->tcp_v4_do_rcv-->tcp_rcv_state_process-->tcp_rcv_synsent_state_process-->tcp_send_synack-->tcp_transmit_skb...
发送SYN包后,socket对应的sock的状态变成TCPF_SYN_SENT,网卡收到服务器的ack传到tcp层时,根据TCPF_SYN_SENT状态,做相关判断后再发送用于第三次握手的ack包。至此,将socket的状态改为连接建立,即TCP_ESTABLISHED。 具体的代码大家可以根据我提供的函数调用链查看。
注意,以TCPF_前缀开头的状态都表示是中间状态,而已TCP_为前缀的状态才是socket的一个相对稳定的状态。
二、服务器端支持
服务器端此时必须是监听状态,则其函数调用链为:
netif_rx-->net_rx_action-->...(IP层处理)-->tcp_v4_rcv-->tcp_v4_do_rcv-->
tcp_rcv_state_process-->tcp_v4_conn_request-->tcp_v4_send_synack...
在tcp_v4_conn_request,中部分代码如下:
case TCP_LISTEN: if(th->ack) /*监听时收到的ack包都丢弃?*/ return 1; if(th->syn) {/*如果是SYN包,则调用tcp_v4_conn_request*/ if(tp->af_specific->conn_request(sk, skb) < 0) return 1; ...