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;
    ...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM