網絡通訊與網絡安全】網絡通訊中的隨機數如果不隨機會怎么樣?(上)

      網友投稿 888 2022-05-30

      1 寫在前言

      最近在排查一個項目的性能壓測問題,十分偶然地發現一個莫名偶現的網絡掉線問題,最后排查發現居然跟系統的隨機數特性有莫大的關系。

      由于我們現在的應用場景都是基于Wi-Fi的網絡連接,所以本文會結合這個偶現的網絡掉線問題,重點分析下在網絡通訊中,如果隨機數不隨機會引發什么問題,以及如何去排查和解決這些問題。

      通過本文的閱讀,你將可以了解到:

      在網絡通訊中,如果隨機數不隨機會引發什么問題?

      MQTT中的keepalive參數有何作用?

      TCP三次握手和四次揮手的過程是怎么樣的?

      lwip協議棧的實現中是如何使用隨機數的?

      嵌入式Wi-Fi設備如何抓取通訊報文?

      如何“重載”標準C庫的rand函數?

      2 問題描述

      我們先來看下當時測試提的issue是怎么說的。

      當我第一時間看到這個問題的時候,就想起來,其實我們的版本還在內測階段的時候,就已經發現了類似的問題,只不過這個問題復現概率相對較低,當時還一度懷疑是偶然的熱點掉線啥的,所以就不了了之了。當時內測的issue記錄如下:

      其中,仔細分析我們內測階段提的issue是可以看出問題的,至少我們可以知道:

      出現問題時,無論云端到終端,還是終端到云端,通訊數據都是不暢通的,這一點可以基本判定設備是掉線的;

      【網絡通訊與網絡安全】網絡通訊中的隨機數如果不隨機會怎么樣?(上)

      出現問題時,排除是網絡中斷的情況;畢竟ping外網是通的;

      issue中都提到了中斷2-3分鐘(感官時間,不是精準計時,精準應該是3分鐘)后,觸發重連機制,重連成功后,問題解除了;

      該問題的觸發時間節點,一定是某次重啟之后的第一次網絡通訊;

      跟具體的云平臺無關,但與具體的模組型號強相關。

      以上就是這些是通過觀看設備的log以及結合一些簡單的測試方法就可以得出的基本結論,但是并不能準確得出結論,為何在這個節點下設備會掉線,或者說,**為何在成功配網后,發起ping包才會發現掉線,前面的配網不是交互得好好的嗎?掉線究竟是設備端主動掉的還是云端關閉連接的?**最重要的是,這種情況能不能規避或者妥善解決?

      帶著這些疑問,我們需要做更進一步的實驗和分析。

      3 場景再現

      3.1 復現環境搭建

      大部分軟件問題難解決主要有兩個方面,一個是難復現或者說找不到穩定復現的路勁,還有一種就是你能找到穩定復現的路勁,但是這個bug解決不了,或者說你解決不了,要么它有外部依賴,要么它就是個已知bug,你就是解決不了。

      說句不好聽的:寫軟件的,誰還沒幾個解決不了的bug?

      但是,說是這樣說,至少你需要去嘗試復現,指不定能找到復現的路徑呢;只有當你的確找到了復現路徑,且使用了各種手段嘗試去解決也沒法解決,哪怕找原廠協助也依然無能為力,我們才能把問題歸為第二類。

      根據issue提供的信息,快速搭建復現環境,嘗試復現。注意,我們在issue的附件log中很清晰地看到出問題的節點下,MQTT的ping包丟了,所以在搭建復現環境的時候,我們嘗試了修改MQTT ping包的發送周期。我們提測的版本用的是典型值60S,所以復測中我們同步修改2個版本,分別是30S和120S。

      好巧不巧,120S的版本,按照正常的配網流程操作個沒幾次,一下子就復現了。這讓我們有點驚呆,不知是運氣好,還是真的這個復現概率就是這么高!!!

      3.2 復現問題的說明

      既然問題很快復現了,我們應該正視問題的排查和分析思路。從復現問題點開始,嘗試ping網關,嘗試ping外網,發現都是通的,難道真的只是一次偶發的網絡掉線?

      為何會有這樣的問號,那是因為辦公室的Wi-Fi網絡環境的確比較差,無線通訊干擾很大,不排除偶然有這種掉線的可能性。

      面對這個復現問題,我們還想到了抓空口包,試著分析當前狀態的空口數據的情況,順帶觀測下當前無線網絡的通暢情況。

      我們也做好了另一份方案,抓網絡包,也就是TCP/IP包;抓這個包的作用主要是觀測問題節點下網絡報文的傳輸情況,曾經在第一時間看到這個issue的時候,還有一個懷疑點就是通訊鏈路斷了,到底斷沒斷,TCP/IP包大概就能看出來。

      以下就是基于復現的問題節點做出的初步排查和分析方案,具體的操作還得看下文后續的分析、解決及驗證。

      4 問題分析

      作為一個嵌入式軟件工程師,我個人認為,當出現問題,首先應該排除硬件的問題,也就是說,先假設設備硬件完好的情況下去分析軟件問題;只有當你把所有的軟件可能性排除得差不多了,或者你在排除的過程中,找到了充分的證據證明硬件問題的可能性非常大,那么這個時候你就可以去找硬件工程師battle battle了。

      其次,排查軟件問題,無非兩個方向,要不從大到小,要么從小到大。從大到小指的是先從宏觀的軟件架構層面去思考和分析,層層剝離,循序漸進,直到分析可能出現的更小范圍,各個排查突破;從小到大指的是從微觀的末端錯誤log開始分析,一步步反推導致這個錯誤的出現的可能性,層層剝離,結合上下文信息深入分析,直至找到問題的根源。

      4.1 從大到小:理解軟件架構

      上面也提到了,從小到大的排查方式是從代碼架構層面去分析;為了聚焦在網絡這一塊,我把原本比較復雜的架構精簡了一下,僅保留與網絡通訊相關的內容,大致如下圖所示:

      從第2章節的issue描述以及第3章節自己的復測,我們可以知道出現問題是在MQTT這個組件中爆發了問題,且在芯片PLATFORM中只有XXX上面才會出現,于是我們可以大膽地假設一個觀點:問題很有可能出現在mbedtls組件或lwip組件!

      同時,由于我們在做架構圖的時候,更多的是在邏輯層面,所以在代碼架構圖中,并沒有很好地對lwip的物理存在做準確的描述。理論上說,物理架構必須是服從于邏輯架構,但在實操過程中,我們在這一原則上的確偷了一下懶,原因就是YYY和XXX都已經移植好了現成的lwip組件,關鍵是他們適配的版本不一樣,所以我們并沒有統一lwip組件,而實際執行的軟件架構圖是下面這張圖:

      配合這個物理架構圖,1路勁沒有問題,而2路勁卻出問題了,基本可以推斷出是lwip組件的問題。

      4.2 從小到大:拋開現象看本質

      從復現的問題現場的末端,最直觀的就是mqtt send ping發出去了,但是沒有mqtt recv pingrsp。

      單從這個現象,我們需要尋找的本質是:

      MQTT模塊是否工作不正常了?MQTT掉線了?MQTT自己斷開掉線還是broker斷開導致的掉線?

      如果MQTT工作不正常,那么TCP層工作是否正常?畢竟MQTT是基于TCP層,在其之上。

      另外,4.1復現問題中,對MQTT的keepalive參數做了調整,是否這個參數有著致命的影響?

      MQTT規范中對keepalive是如何描述的?

      一個簡單的現象,要看清其本質并不容易,需要下面大量的輔助分析過程。

      就像這樣:

      MQTT掉線 --》PINGREQ包發出去了嗎?–》PINGRESP包收到了嗎?–》TCP鏈接什么情況?–》空口通訊是否正常?

      4.3 要放大招:三板斧出擊

      從上面的都僅僅是初步的假設分析,還沒法找到真正的證據;再要深入細節,底層的log以及網絡報文肯定少不了。

      4.3.1 第一板斧:MQTT log

      我們使用的是pahu的C語言版本的MQTT,通過瀏覽器代碼實現,我們可以知道其MQTT層的log開關位于:

      打開這里的開關,我們就可以看到更多細致的MQTT log,包括MQTT基礎報文的收發都可以看到。這個就可以相對清晰地知道,在發生MQTT掉線(ping lost)的時候,究竟有沒有收到ping resp?

      有一種情況是的確沒有收到,這種肯定是lost;還有一種是,可能收到了,但是在MQTT層解析、拆包、校驗的時候發現是一個非法包,然后直接丟棄了,不能丟到上層去處理。通常來說,第二種情況比較少見。

      還有一點,我們使用的MQTT實現包中對MQTT收到的報文,全部都是在mqtt_yield(Client, timeout_ms)查詢式接收,當收到一個有效的MQTT報文,會有類似下面一段的處理代碼:

      // check recv MQTT packet type switch (packetType) { case CONNACK: { mqtt_debug("CONNACK"); break; } #if !WITH_MQTT_ONLY_QOS0 case PUBACK: { mqtt_debug("PUBACK"); rc = iotx_mc_handle_recv_PUBACK(c); if (SUCCESS_RETURN != rc) { mqtt_err("recvPubackProc error,result = %d", rc); } break; } #endif case SUBACK: { mqtt_debug("SUBACK"); rc = iotx_mc_handle_recv_SUBACK(c); if (SUCCESS_RETURN != rc) { mqtt_err("recvSubAckProc error,result = %d", rc); } break; } case PUBLISH: { mqtt_debug("PUBLISH"); /* HEXDUMP_DEBUG(c->buf_read, 32); */ rc = iotx_mc_handle_recv_PUBLISH(c); if (SUCCESS_RETURN != rc) { mqtt_err("recvPublishProc error,result = %d", rc); } break; } case UNSUBACK: { mqtt_debug("UNSUBACK"); rc = iotx_mc_handle_recv_UNSUBACK(c); if (SUCCESS_RETURN != rc) { mqtt_err("recvUnsubAckProc error,result = %d", rc); } break; } case PINGRESP: { rc = SUCCESS_RETURN; mqtt_info("receive ping response!"); break; } default: mqtt_err("INVALID TYPE"); _reset_recv_buffer(c); HAL_MutexUnlock(c->lock_read_buf); return FAIL_RETURN; }

      倘若正常收到ping回復的,一定會有"receive ping response!"的log輸出,這也是斷定MQTT是否掉線的一個簡單判斷。

      4.3.2 第三板斧:TCP/IP抓包

      由于我們使用的是Wi-Fi網絡通訊,所以要想抓取模組的TCP/IP報文,通常有以下幾種方法:

      方法1:在無線路由器中抓取流過路由器的報文,這種方法對路由器有要求,實踐中,我們并沒有采取這種方法,感興趣可以去了解下。

      方法2:利用中間人原理來抓包,以前我就曾經使用過這個方法抓一些蜂窩網絡的網絡報文,效果還是不錯的,只不過代碼層面需要稍作點服務器的地址、端口修改,它的原理如下圖所示。它有個弊端,就是需要一個具備抓包環境的公網服務器;同時在公網PC端需要一個代理軟件,這里推薦使用一個叫sockit的開源軟件,感興趣可以了解下。

      方法3:利用無線熱點的功能特性來抓包,它的原理如下圖所示,大家一看便懂,其實就是PC電腦使用無線網卡或類似360Wi-Fi這種,開啟一個無線AP熱點,讓設備連接這個無線熱點,從而達到探測網絡報文的目的。不過,它也是多少有些缺陷,感興趣可以了解下,但是基本應付我們這種抓包場景肯定是沒有問題的。

      經綜合考慮,我們采用的是方法3來抓包,配合前面提及的復現方法,很快就抓到了對應的TCP報文(感興趣的可以去這里取報文)。

      通過這種方式抓包會把PC上所有的網絡報文中抓包,為了精準展示設備的報文,我們需要對所抓的報文進行過濾,使用的過濾指令是 “tcp.port=xxx && ip.addr=yyy.yyy.yyy.yyy“,其中xxx表示設備端鏈接服務器端的端口號,yyy.yyy.yyy.yyy是服務器主機的IP地址;如果服務器是域名的形式的話,先在PC上使用ping命令把域名解析成IP。

      wireshark中對報文的過濾操作,如下圖所示:

      通過wireshark簡單一看,找到對應ping lost的時間節點,MQTT的ping包看似壓根就沒發出去,因為ping包在TCP層一直是重傳的,壓根得不到服務器的ACK。

      如下所示:

      4.3.3 第二板斧:空口抓包

      空口抓包,我們使用的是omnipeek軟件,這也是業內常規使用的空口抓包工具。

      關于如何搭建omnipeek的抓包環境,我這里不再贅述,感興趣的可以科學上網,找一些參考教程,一學便會。

      它的抓包界面長這樣:

      具體解析的數據幀解析界面長這樣:

      如不習慣使用它來看報文,倒是可以導出其網絡包,使用wireshark來看網絡報文,也是一種常見的分析手段。

      有了omnipeek的抓包環境,配合前面的復現方法,我們發現當問題出現時,omnipeek是能抓到一些TCP報文流過的,這至少能說明,在問題節點下空口通訊是正常的,需要再往上層協議去排查。

      4.3.4 分析小結

      看這里好像是三板斧分三個階段走,在實操過程中,其實三板斧是同時進行的,這也是為了能夠在問題節點下分析出更多的線索和可能性。三者是相輔相成的,都聯系在一起。

      4.4 關鍵轉機:找到突破口

      誰來也巧,在上面抓TCP包分析的時候,我們可以看到MQTT ping包變成了Application Data,為什么?

      原因在于我們在MQTT層上加了TLS,實際上跑的MQTTS;我們的實現是:MQTT+mbedtls。

      我當時有個想法就是,能不能把MQTTS中的密文解開來,看著也舒服些,遇到開始查找資料,找到了這篇參考教程,是RT-Thread輸出的教程:基于RT-Thread 使用 wireshark 抓取 HTTPS 數據包。

      它的思路很新穎也很聰明,實現原理圖長這樣:

      使用這個方案, 電腦創建 一個Wi-Fi 熱點,設備端連接電腦熱點,并發起 https 請求(TLS),服務器接收到請求,向設備端發出響應,設備端根據響應的內容,計算出密鑰, 并將設備端隨機數和密鑰通過 udp 發送到 pc,保存到 sslkey.log 文件,wireshark 根據設備端隨機數和密鑰即可將TLS 數據包解密。

      其核心邏輯就是讓處于抓包狀態的wireshark拿到設備與服務器端最終協商的那個數據加密的key,從而把密文的數據還原成明文。

      參考教程,我很快就把相應的流程跑起來了,但是遺憾的是wireshark并沒能成功地幫我解開密文數據。

      不過也不是完全一無所獲,因為我發現了一個致命的問題在里面,這個致命問題倒是給我提供了一個新思路,真是塞翁失馬焉知非福!

      在以前的金融POS機器安全研發的工作經歷中,我曾經花很大的力氣專門研究過TLS握手相關的握手以及數據的加解密流程,所以對上述教程中提及的TLS相關的講解,也是理解得比較透徹。

      但我發現其中的致命問題是,我從設備截獲的CLIENT RANDOM字段保存在sslkey.log中,居然每次開機都是一模一樣的:

      這肯定不行啊!要知道這可是TLS握手中客戶端的隨機數啊?怎么能每次都一樣呢?豈不是會被人重放攻擊?

      這種情況下,要么是mbedtls庫實現有問題,要不就是隨機數有問題?

      既然mbedtls別人用了那么多,而且我們其他芯片平臺也用啊,也沒遇到這種問題,所以隨機數的可能就非常大了!

      也確認了下mbedtls中使用隨機數的最終調用接口:

      static unsigned int _avRandom() { return (((unsigned int)rand() << 16) + rand()); } static int _ssl_random(void *p_rng, unsigned char *output, size_t output_len) { uint32_t rnglen = output_len; uint8_t rngoffset = 0; while (rnglen > 0) { *(output + rngoffset) = (unsigned char)_avRandom(); rngoffset++; rnglen--; } return 0; } // mbedtls connection init { // ... mbedtls_ssl_conf_rng(&(pTlsData->conf), _ssl_random, NULL); // ... }

      WC !居然是標準C庫的rand函數!這!!!

      直到這里,我才正兒八經地往隨機數的方向去懷疑了,最后的實踐證明,這個思路恰好對了。

      隨機數這個思路一打開之后,我突然想起大概2個月前幫Wi-Fi組的同事排查過一個lwip隨機數引發的問題,但是腦子里有些模糊,只記得好像會引發斷線啥的。

      果然找到對口的同事(還在隔離中),語音確認了一波,果然問題的現象我們這無比的相應,要知道他當時調的芯片平臺和SDK都不是我現在用的這套,這就足以證明,這個問題是首次在我們的SDK和芯片平臺上爆發,而且這個問題估計原廠還未同步發現。

      4.5 知識點補缺

      上面的思路,已經將疑點對準隨機數了,但是為了能準確分析解決問題,我們需要將相關的理論知識惡補以下。

      4.5.1 MQTT的心跳機制

      這種純理論知識,我想沒有什么比MQTT的協議規范更有說服力,于是我查找了MQTT-V3.1.1的規范文檔,找到了相關說明:

      keepalive參數

      PINGREQ報文和PINGRESP報文

      簡單總結下:

      當客戶端啟動了keepalive特性之后,客戶端至少應在keepalive間隔內發起一條PINGREQ,如果服務端在一點五倍的保持連接時間內沒有收到客戶端的控制報文,它必須斷開客戶端的網絡連接,認為網絡連接已斷開。反之,如果服務器收到了PINGREQ,就必須響應PINGRESP以表示自己還活著。

      4.5.2 lwip協議棧

      lwip是一個非常輕量級的TCP/IP協議棧的C版本實現,它在有無操作系統的支持都可以運行。LwIP實現的重點是在保持TCP協議主要功能的基礎上減少對RAM 的占用,它只需十幾KB的RAM和40K左右的ROM就可以運行,這使LwIP協議棧適合在低端的嵌入式系統中使用。更多簡要介紹,可以參考(百度百科)[https://baike.baidu.com/item/LwIP/10694326].

      對于lwip的使用,我們已經很熟悉了,因為它兼容原生的BSD socket,很容易就可以基于socket API把網絡程序給跑起來。同時,原廠已經幫忙把lwip在指定的RTOS系統(本案例是freeRTOS)中,但我們應該好好學一學lwip移植相關的內容,可以參考下這里。

      我這里重點提及下它使用隨機數的地方,關于它的初始化流程可以參見這里。

      在它的初始化流程中,需要執行到一個tcp_init的函數,位于tcp.c中:

      //init.c void lwip_init(void) { #ifndef LWIP_SKIP_CONST_CHECK int a; LWIP_UNUSED_ARG(a); LWIP_ASSERT("LWIP_CONST_CAST not implemented correctly. Check your lwIP port.", LWIP_CONST_CAST(void*, &a) == &a); #endif #ifndef LWIP_SKIP_PACKING_CHECK LWIP_ASSERT("Struct packing not implemented correctly. Check your lwIP port.", sizeof(struct packed_struct_test) == PACKED_STRUCT_TEST_EXPECTED_SIZE); #endif /* Modules initialization */ stats_init(); #if !NO_SYS sys_init(); #endif /* !NO_SYS */ mem_init(); memp_init(); pbuf_init(); netif_init(); #if LWIP_IPV4 ip_init(); #if LWIP_ARP etharp_init(); #endif /* LWIP_ARP */ #endif /* LWIP_IPV4 */ #if LWIP_RAW raw_init(); #endif /* LWIP_RAW */ #if LWIP_UDP udp_init(); #endif /* LWIP_UDP */ #if LWIP_TCP tcp_init(); #endif /* LWIP_TCP */ #if LWIP_IGMP igmp_init(); #endif /* LWIP_IGMP */ #if LWIP_DNS dns_init(); #endif /* LWIP_DNS */ #if PPP_SUPPORT ppp_init(); #endif #if LWIP_TIMERS sys_timeouts_init(); #endif /* LWIP_TIMERS */ } //tcp.c /** * Initialize this module. */ void tcp_init(void) { #if LWIP_RANDOMIZE_INITIAL_LOCAL_PORTS && defined(LWIP_RAND) tcp_port = TCP_ENSURE_LOCAL_PORT_RANGE(LWIP_RAND()); //關鍵操作:初始化的時候隨機取得tcp_port os_printf("tcp_port:%d\r\n", tcp_port); #endif /* LWIP_RANDOMIZE_INITIAL_LOCAL_PORTS && defined(LWIP_RAND) */ }

      OK,我們這里看到它使用了一個LWIP_RAND操作,而原廠適配lwip的時候并沒有把這個LWIP_RAND切換到硬件的RAND,而是用了標準C庫的rand函數,前面已經有跡象表明,它就不是隨機的,這里還用?

      tcp_init無非是取得一個tcp_port的基準偏移,后面在創建客戶端的時候,對服務器發起TCP鏈接,本地的端口號就是根據這個tcp_port來計算出來的,代碼如下:

      //tcp.c /** * Allocate a new local TCP port. * * @return a new (free) local TCP port number */ static u16_t tcp_new_port(void) { u8_t i; u16_t n = 0; struct tcp_pcb *pcb; again: //關鍵操作:tcp_port+1獲得新的端口號 if (tcp_port++ == TCP_LOCAL_PORT_RANGE_END) { tcp_port = TCP_LOCAL_PORT_RANGE_START; } /* Check all PCB lists. */ for (i = 0; i < NUM_TCP_PCB_LISTS; i++) { for (pcb = *tcp_pcb_lists[i]; pcb != NULL; pcb = pcb->next) { if (pcb->local_port == tcp_port) { if (++n > (TCP_LOCAL_PORT_RANGE_END - TCP_LOCAL_PORT_RANGE_START)) { return 0; } goto again; } } } return tcp_port; }

      所以,到這基本就解釋了,重啟后的那次TCP鏈接為何使用了前一次TCP鏈接的端口號,因為tcp_port兩次(很有可能)是一樣的。

      4.5.3 TCP的狀態圖

      要熟練地分析上面的各個場景,務必需要對TCP的各個狀態非常了解。從網上找了一張關于TCP狀態介紹稍全的圖,供大家參考下:

      關于TCP的狀態切換圖,我也還在學習,期間我找大神(小林coding)討論過這個有趣的問題,原來他之前寫過這個場景的分析,那我就直接搬過來了,感興趣的可以一看。

      他的核心觀點就是:

      處于 establish 狀態的服務端如果收到了客戶端的 SYN 報文(注意此時的 SYN 報文其實是亂序的,因為 SYN 報文的初始化序列號其實是一個隨機數),會回復一個攜帶了正確序列號和確認號的 ACK 報文,這個 ACK 被稱之為 Challenge ACK。

      接著,客戶端收到這個 Challenge ACK,發現序列號并不是自己期望收到的,于是就會回 RST 報文,服務端收到后,就會釋放掉該連接。

      結合我們抓的TCP報文,這不就是剛好驗證了我們的復現場景嗎?

      4.5.4 TCP報文的標志位

      TCP的報文中規定有6種重要的標志位:

      URG:(Urgent Pointer field significant)緊急指針。用到的時候值為1,用來處理避免TCP數據流中斷。【這個標志位很少見】

      ACK:(Acknowledgment fieldsignificant)置1時表示確認號(AcknowledgmentNumber)為合法,為0的時候表示數據段不包含確認信息,確認號被忽略。

      PSH:(Push Function),PUSH標志的數據,置1時請求的數據段在接收方得到后就可直接送到應用程序,而不必等到緩沖區滿時才傳送。

      RST:(Reset the connection)用于復位因某種原因引起出現的錯誤連接,也用來拒絕非法數據和請求。如果接收到RST位時候,通常發生了某些錯誤。

      SYN:(Synchronize sequence numbers)用來建立連接,在連接請求中,SYN=1,ACK=0,連接響應時,SYN=1,ACK=1。即,SYN和ACK來區分Connection Request和Connection Accepted。

      FIN:(No more data from sender)用來釋放連接,表明發送方已經沒有數據發送了。

      熟悉這幾個標志位的基礎含義,基本上就可以看懂一段TCP網絡報文了。

      4.6 深入分析:從理論分析到實戰分析

      有了上面的知識點補充,我們嘗試著深入分析,看看把這些知識點結合實際的案例場景串起來?。

      4.6.1 理論分析:理論上的復現路徑

      從lwip的初始化分析,我們可以知道在設備重開機后,設備發起的第一筆TCP鏈接使用的端口是跟其初始化的tcp_port有直接的關系(tcp_port + 1);而我們的Wi-Fi設備都是連接的無線路由熱點的,所以設備重啟后,很大可能也是取到同一個子網IP。這樣的話,重啟前后的兩次TCP鏈接使用的四元組就是完全相同的:(客戶端端口號、客戶端本地IP、服務端端口號、服務器IP)。

      會發生什么事情,我直接用小林的一張圖來說明:

      處于 establish 狀態的服務端如果收到了客戶端的 SYN 報文(注意此時的 SYN 報文其實是亂序的,因為 SYN 報文的初始化序列號其實是一個隨機數),會回復一個攜帶了正確序列號和確認號的 ACK 報文,這個 ACK 被稱之為 Challenge ACK。

      接著,客戶端收到這個 Challenge ACK,發現序列號并不是自己期望收到的,于是就會回 RST 報文,服務端收到后,就會釋放掉該連接。

      他的博文中是分析了linux系統下的TCP協議對這種場景的報文回復情況,那么我試著從lwip協議棧的實現中,找找相關的處理是怎么樣的。

      當客戶端發起tcp connect的時候,調用的是lwip_connect,具體可以參考下面。

      函數調用順序:-> lwip_connect

      -> netconn_connect

      -> netconn_apimsg

      -> lwip_netconn_do_connect

      -> tcp_connect

      -> …

      err_t tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port, tcp_connected_fn connected) { err_t ret; u32_t iss; u16_t old_local_port; // 省略部分實現 /* Send a SYN together with the MSS option. */ ret = tcp_enqueue_flags(pcb, TCP_SYN); if (ret == ERR_OK) { /* SYN segment was enqueued, changed the pcbs state now */ pcb->state = SYN_SENT; if (old_local_port != 0) { TCP_RMV(&tcp_bound_pcbs, pcb); } TCP_REG_ACTIVE(pcb); MIB2_STATS_INC(mib2.tcpactiveopens); tcp_output(pcb); } return ret; }

      通過tcp_connect這樣就可以看到lwip在組一個帶有SYN的TCP報文,通過底層的接口發送出去,同時將TCP的狀態切換到SYN_SENT狀態。

      由于我們實現的lwip是異步模式,所以最終接收對方的響應報文在tcp_in.c里面,我們注意到有這么一個函數tcp_process,它就是TCP狀態的狀態機實現函數。

      函數調用順序:-> tcp_input

      -> tcp_process …

      /** * Implements the TCP state machine. Called by tcp_input. In some * states tcp_receive() is called to receive data. The tcp_seg * argument will be freed by the caller (tcp_input()) unless the * recv_data pointer in the pcb is set. * * @param pcb the tcp_pcb for which a segment arrived * * @note the segment which arrived is saved in global variables, therefore only the pcb * involved is passed as a parameter to this function */ static err_t tcp_process(struct tcp_pcb *pcb) { struct tcp_seg *rseg; u8_t acceptable = 0; err_t err; err = ERR_OK; //忽略部分代碼 /* Do different things depending on the TCP state. */ switch (pcb->state) { case SYN_SENT: LWIP_DEBUGF(TCP_INPUT_DEBUG, ("SYN-SENT: ackno %"U32_F" pcb->snd_nxt %"U32_F" unacked %"U32_F"\n", ackno, pcb->snd_nxt, lwip_ntohl(pcb->unacked->tcphdr->seqno))); /* received SYN ACK with expected sequence number? */ if ((flags & TCP_ACK) && (flags & TCP_SYN) && (ackno == pcb->lastack + 1)) { pcb->rcv_nxt = seqno + 1; pcb->rcv_ann_right_edge = pcb->rcv_nxt; pcb->lastack = ackno; pcb->snd_wnd = tcphdr->wnd; pcb->snd_wnd_max = pcb->snd_wnd; pcb->snd_wl1 = seqno - 1; /* initialise to seqno - 1 to force window update */ pcb->state = ESTABLISHED; #if TCP_CALCULATE_EFF_SEND_MSS pcb->mss = tcp_eff_send_mss(pcb->mss, &pcb->local_ip, &pcb->remote_ip); #endif /* TCP_CALCULATE_EFF_SEND_MSS */ pcb->cwnd = LWIP_TCP_CALC_INITIAL_CWND(pcb->mss); LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_process (SENT): cwnd %"TCPWNDSIZE_F " ssthresh %"TCPWNDSIZE_F"\n", pcb->cwnd, pcb->ssthresh)); LWIP_ASSERT("pcb->snd_queuelen > 0", (pcb->snd_queuelen > 0)); --pcb->snd_queuelen; LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_process: SYN-SENT --queuelen %"TCPWNDSIZE_F"\n", (tcpwnd_size_t)pcb->snd_queuelen)); rseg = pcb->unacked; if (rseg == NULL) { /* might happen if tcp_output fails in tcp_rexmit_rto() in which case the segment is on the unsent list */ rseg = pcb->unsent; LWIP_ASSERT("no segment to free", rseg != NULL); pcb->unsent = rseg->next; } else { pcb->unacked = rseg->next; } tcp_seg_free(rseg); /* If there's nothing left to acknowledge, stop the retransmit timer, otherwise reset it to start again */ if (pcb->unacked == NULL) { pcb->rtime = -1; } else { pcb->rtime = 0; pcb->nrtx = 0; } /* Call the user specified function to call when successfully * connected. */ TCP_EVENT_CONNECTED(pcb, ERR_OK, err); if (err == ERR_ABRT) { return ERR_ABRT; } tcp_ack_now(pcb); } /* received ACK? possibly a half-open connection */ else if (flags & TCP_ACK) { /* send a RST to bring the other side in a non-synchronized state. */ tcp_rst(ackno, seqno + tcplen, ip_current_dest_addr(), ip_current_src_addr(), tcphdr->dest, tcphdr->src); /* Resend SYN immediately (don't wait for rto timeout) to establish connection faster, but do not send more SYNs than we otherwise would have, or we might get caught in a loop on loopback interfaces. */ if (pcb->nrtx < TCP_SYNMAXRTX) { pcb->rtime = 0; tcp_rexmit_rto(pcb); } } break; //忽略其他代碼 return ERR_OK; }

      函數比較長,我們抓重點,它這里就是根據當前TCP的不同狀態做不同的處理。我們看到第80行,這里:

      看注釋很清晰,當TCP的狀態是SYN_SENT狀態的時候,收到一個只帶ACK的報文,那么它就會回應一個RST報文,同時快速重傳一個SYN報文。

      接著這個函數,我們看下服務器端的處理,如果TCP鏈接已經處于ESTABLISHED狀態,當它收到SYN報文時,它會怎么處理呢?

      /** * Implements the TCP state machine. Called by tcp_input. In some * states tcp_receive() is called to receive data. The tcp_seg * argument will be freed by the caller (tcp_input()) unless the * recv_data pointer in the pcb is set. * * @param pcb the tcp_pcb for which a segment arrived * * @note the segment which arrived is saved in global variables, therefore only the pcb * involved is passed as a parameter to this function */ static err_t tcp_process(struct tcp_pcb *pcb) { struct tcp_seg *rseg; u8_t acceptable = 0; err_t err; err = ERR_OK; /* Process incoming RST segments. */ if (flags & TCP_RST) { /* First, determine if the reset is acceptable. */ if (pcb->state == SYN_SENT) { /* "In the SYN-SENT state (a RST received in response to an initial SYN), the RST is acceptable if the ACK field acknowledges the SYN." */ if (ackno == pcb->snd_nxt) { acceptable = 1; } } else { /* "In all states except SYN-SENT, all reset (RST) segments are validated by checking their SEQ-fields." */ if (seqno == pcb->rcv_nxt) { acceptable = 1; } else if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, pcb->rcv_nxt + pcb->rcv_wnd)) { //在接收窗口內的RST報文,最終是在這里處理!!! /* If the sequence number is inside the window, we only send an ACK and wait for a re-send with matching sequence number. This violates RFC 793, but is required to protection against CVE-2004-0230 (RST spoofing attack). */ tcp_ack_now(pcb); } } if (acceptable) { LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: Connection RESET\n")); LWIP_ASSERT("tcp_input: pcb->state != CLOSED", pcb->state != CLOSED); recv_flags |= TF_RESET; pcb->flags &= ~TF_ACK_DELAY; return ERR_RST; } else { LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: unacceptable reset seqno %"U32_F" rcv_nxt %"U32_F"\n", seqno, pcb->rcv_nxt)); LWIP_DEBUGF(TCP_DEBUG, ("tcp_process: unacceptable reset seqno %"U32_F" rcv_nxt %"U32_F"\n", seqno, pcb->rcv_nxt)); return ERR_OK; } } //當服務器端收到一個處于ESTABLISHED狀態的連接收到一個SYN報文,就直接回復ACK報文了。 if ((flags & TCP_SYN) && (pcb->state != SYN_SENT && pcb->state != SYN_RCVD)) { /* Cope with new connection attempt after remote end crashed */ tcp_ack_now(pcb); return ERR_OK; } //忽略部分代碼

      結合下面的實際抓包,我們再仔細分析分析。

      4.6.2 實戰分析:實戰中的場景路勁

      文不如圖,針對真實的場景路徑,我想我直接從所抓到的TCP報文來入手分析可能會效果更好。

      下面幾張圖,是從復現問題的報文中截取出來的,我分為了以下三部分:(完整報文戳這里)

      開機正常連上服務器,正常收到報文,PING包能發能收

      這里可以看到報文序號**#1使用端口號26947**去連接服務器,一切正常,后面交互PING包也非常正常。

      設備重啟后,連接服務器,后面開始出現掉線

      這里我們注意報文序號**#41-#47**,這個時間節點,就是設備重啟后首次發起(第一次)TCP連接,我們可以清晰地看到,它使用的端口號仍然是26947,與重啟前的端口號是一樣的,這不就進入到前一小節的理論分析中了嗎?

      我們再仔細看下,這個時候,報文交互上發生了什么?

      41號報文,使用帶SYN=1且Seq為0(相對值為0)的的報文發起TCP連接,緊接著#42報文,服務器端回應了一個ACK報文(Seq=4670,ACK=1284),隨后#43報文,設備端認為服務器回復的不對,從而發出了帶RST的鏈接重置的報文。

      熟悉TCP鏈接的三次握手,我們都知道,正常的握手流程應該是:SYN(seq=0,ACK=0) -> SYN,ACK(seq=0,ACK=1) -> ACK(seq=1,ACK=1);而我們看到的這次三次握手卻不是我們的期望的。

      我們重點看看,服務器端在回應客戶端SYN報文回復的這個報文,究竟是啥意思。Seq=4670,ACK=1284,意味著服務器還認為客戶端給過去的報文交互,還是重啟前那一次的呢;ACK=1284表示服務器對前1284個字節都已經收到了,所以呢wireshark也很聰明,直接把客戶端的SYN報文標記為TCP Retransmission(報文重傳:它認為#41報文時#1報文的重傳),而服務端回應SYN的報文標記為TCP Dup ACK #39-1(重復ACK確認:它認為服務器對#39號報文重復確認了,因為它們都是ACK=1284)。

      接下來是最重要的一條報文#42號RST報文:根據TCP的標志位介紹,我們可以知道這條報文客戶端是想重置這個鏈接,也就是它要廢棄這個服務器認為正常的TCP鏈接,但似乎服務器并不買單,我們繼續看下面的報文。

      期間能正常收到服務器的推送(設備收到MQTT推送arrived的log也可以佐證這一點),直到#73 #74報文客戶端需要發PING包的時候,發現掉線了。

      觸發掉線重連機制,重新連上服務器

      看著三次握手多順利,同時我們留意到這次的客戶端端口不再是26947了,而是一個新的端口號26946;這是因為抓包方式的原因,這個端口號并不完全體現是設備端lwip的tcp_port,但至少是能反映它是在變化的。

      重連成功后,設備重新在線,PING包交互正常,恢復了。

      4.6.3 解決疑惑:為何偶現而不必現

      既然上面分析得頭頭是道,照這么說應該是一個必現的問題呀?為何在實際生產案例中,卻是偶現的呢?難道還有什么因素我們沒考慮進去?

      首先,在上面的分析中,我們得出一個很重要的結論,當服務器端還處于連接狀態的TCP鏈接,收到一個由相同的四元組組成的SYN報文,最終就會觸發設備端產生RST報文,從而使得通訊鏈接發生“假鏈接”,影響實際通訊!

      在這個結論中,有幾個前提必須要滿足:

      相同的四元組構成的SYN報文;

      前一個鏈接在TCP服務端還處于TCP狀態中的已鏈接狀態。

      短時間內連接同一個無線路由,很大概率獲取同一個本地IP,由于隨機數的問題,本地端口也是同一個,所以第一條相同四元組是很容易滿足的,第二條需要滿足前一鏈接還保持在已鏈接狀態,這就要求兩次間隔重啟不能間隔時間太長,否則就會觸發服務器端的掉線檢測機制,從而被識別到設備端已掉線,那么這種情況下,肯定不能復現如題的問題。

      但是,我們在復測的過程,發現有時緊挨著時間重啟,也沒有發生類似的掉線問題,也就是說重啟后的鏈接一樣是好好的。

      通過抓包來看,唯一不同的是沒有出問題的這個重啟,客戶端發起SYN報文,最后并沒有觸發客戶端發送RST報文,如下圖所示:

      而異常的場景下,報文如下:

      這個的確令我百思不得解,看來TCP理論知識還不夠扎實,還要再去惡補惡補。有分析思路的朋友,也歡迎在評論席與我一同討論。

      。。。未完待續。。。

      MQTT 網絡

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel表格批注不顯示的解決教程(excel表格中批注不顯示怎么辦?)
      下一篇:為命令行構建 Python 目錄樹生成器(命令行創建)
      相關文章
      亚洲色无码一区二区三区| 亚洲国产综合精品| 亚洲中文字幕无码爆乳| 亚洲国产精品久久人人爱| 久久国产亚洲高清观看| 亚洲AV一宅男色影视| 九月丁香婷婷亚洲综合色| 亚洲日韩国产精品第一页一区| 久久精品亚洲男人的天堂| 亚洲性日韩精品一区二区三区| 亚洲区日韩区无码区| 亚洲av无码成人精品区在线播放| 国产成人亚洲精品无码AV大片| 日韩在线视精品在亚洲| 色九月亚洲综合网| 亚洲成?v人片天堂网无码| 亚洲午夜爱爱香蕉片| 中文字幕精品亚洲无线码一区应用| 国产精品亚洲产品一区二区三区| 亚洲综合精品网站在线观看| 国产精品亚洲综合一区| 久久亚洲2019中文字幕| 亚洲精品国产品国语在线| 亚洲爆乳无码一区二区三区| 亚洲AV日韩AV永久无码绿巨人| 亚洲国产女人aaa毛片在线 | 亚洲国产精品成人AV无码久久综合影院| 在线亚洲精品视频| 亚洲精品国产电影| 亚洲无码在线播放| 久久噜噜噜久久亚洲va久| 亚洲黄色在线观看视频| 亚洲视频一区二区三区四区| 亚洲日本VA午夜在线电影| 国产偷国产偷亚洲高清人| 国产亚洲自拍一区| 亚洲电影一区二区| 亚洲三级视频在线| 亚洲AV无码XXX麻豆艾秋| 国产成人高清亚洲| 亚洲国产成人久久精品影视|