TCP三次握手机制分析及其实现

一、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<<sk->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;
    ...

猜你喜欢

转载自www.cnblogs.com/z501938568/p/12090866.html