0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

RTThread物聯(lián)網(wǎng)操作系統(tǒng) ? 來源:RTThread物聯(lián)網(wǎng)操作系統(tǒng) ? 作者:RTThread物聯(lián)網(wǎng)操作 ? 2022-03-10 11:51 ? 次閱讀

1 寫在前言

最近在排查一個項目的性能壓測問題,十分偶然地發(fā)現(xiàn)一個莫名偶現(xiàn)的網(wǎng)絡(luò)掉線問題,最后排查發(fā)現(xiàn)居然跟系統(tǒng)的隨機數(shù)特性有莫大的關(guān)系。

由于我們現(xiàn)在的應(yīng)用場景都是基于Wi-Fi的網(wǎng)絡(luò)連接,所以本文會結(jié)合這個偶現(xiàn)的網(wǎng)絡(luò)掉線問題,重點分析下在網(wǎng)絡(luò)通訊中,如果隨機數(shù)不隨機會引發(fā)什么問題,以及如何去排查和解決這些問題。

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

  • 在網(wǎng)絡(luò)通訊中,如果隨機數(shù)不隨機會引發(fā)什么問題?

  • MQTT中的keepalive參數(shù)有何作用?

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

  • lwip協(xié)議棧的實現(xiàn)中是如何使用隨機數(shù)的?

  • 嵌入式Wi-Fi設(shè)備如何抓取通訊報文?

  • 如何“重載”標(biāo)準(zhǔn)C庫的rand函數(shù)?

2 問題描述

我們先來看下當(dāng)時測試提的issue是怎么說的。

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

  • 出現(xiàn)問題時,無論云端到終端,還是終端到云端,通訊數(shù)據(jù)都是不暢通的,這一點可以基本判定設(shè)備是掉線的;

  • 出現(xiàn)問題時,排除是網(wǎng)絡(luò)中斷的情況;畢竟ping外網(wǎng)是通的;

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

  • 該問題的觸發(fā)時間節(jié)點,一定是某次重啟之后的第一次網(wǎng)絡(luò)通訊;

  • 跟具體的云平臺無關(guān),但與具體的模組型號強相關(guān)

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

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

3 場景再現(xiàn)

3.1 復(fù)現(xiàn)環(huán)境搭建

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

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

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

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

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

3.2 復(fù)現(xiàn)問題的說明

既然問題很快復(fù)現(xiàn)了,我們應(yīng)該正視問題的排查和分析思路。從復(fù)現(xiàn)問題點開始,嘗試ping網(wǎng)關(guān),嘗試ping外網(wǎng),發(fā)現(xiàn)都是通的,難道真的只是一次偶發(fā)的網(wǎng)絡(luò)掉線?

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

面對這個復(fù)現(xiàn)問題,我們還想到了抓空口包,試著分析當(dāng)前狀態(tài)的空口數(shù)據(jù)的情況,順帶觀測下當(dāng)前無線網(wǎng)絡(luò)的通暢情況。

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

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

4 問題分析

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

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

4.1 從大到?。豪斫廛浖軜?gòu)

上面也提到了,從小到大的排查方式是從代碼架構(gòu)層面去分析;為了聚焦在網(wǎng)絡(luò)這一塊,我把原本比較復(fù)雜的架構(gòu)精簡了一下,僅保留與網(wǎng)絡(luò)通訊相關(guān)的內(nèi)容,大致如下圖所示:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

4.2 從小到大:拋開現(xiàn)象看本質(zhì)

從復(fù)現(xiàn)的問題現(xiàn)場的末端,最直觀的就是mqtt send ping發(fā)出去了,但是沒有mqtt recv pingrsp。

單從這個現(xiàn)象,我們需要尋找的本質(zhì)是:

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

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

另外,4.1復(fù)現(xiàn)問題中,對MQTT的keepalive參數(shù)做了調(diào)整,是否這個參數(shù)有著致命的影響?

MQTT規(guī)范中對keepalive是如何描述的?

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

就像這樣:

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

4.3 要放大招:三板斧出擊

從上面的都僅僅是初步的假設(shè)分析,還沒法找到真正的證據(jù);再要深入細(xì)節(jié),底層的log以及網(wǎng)絡(luò)報文肯定少不了。

4.3.1 第一板斧:MQTT log

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

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

還有一點,我們使用的MQTT實現(xiàn)包中對MQTT收到的報文,全部都是在mqtt_yield(Client, timeout_ms)查詢式接收,當(dāng)收到一個有效的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回復(fù)的,一定會有"receive ping response!"的log輸出,這也是斷定MQTT是否掉線的一個簡單判斷。

4.3.2 第三板斧:TCP/IP抓包

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

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

如下所示:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

4.3.3 第二板斧:空口抓包

空口抓包,我們使用的是omnipeek軟件,這也是業(yè)內(nèi)常規(guī)使用的空口抓包工具。

關(guān)于如何搭建omnipeek的抓包環(huán)境,我這里不再贅述,感興趣的可以科學(xué)上網(wǎng),找一些參考教程,一學(xué)便會。

它的抓包界面長這樣:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

具體解析的數(shù)據(jù)幀解析界面長這樣:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

如不習(xí)慣使用它來看報文,倒是可以導(dǎo)出其網(wǎng)絡(luò)包,使用wireshark來看網(wǎng)絡(luò)報文,也是一種常見的分析手段。

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

4.3.4 分析小結(jié)

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

4.4 關(guān)鍵轉(zhuǎn)機:找到突破口

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

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

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

其核心邏輯就是讓處于抓包狀態(tài)的wireshark拿到設(shè)備與服務(wù)器端最終協(xié)商的那個數(shù)據(jù)加密的key,從而把密文的數(shù)據(jù)還原成明文。

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

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

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

這肯定不行?。∫肋@可是TLS握手中客戶端的隨機數(shù)???怎么能每次都一樣呢?豈不是會被人重放攻擊?

這種情況下,要么是mbedtls庫實現(xiàn)有問題,要不就是隨機數(shù)有問題?

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

也確認(rèn)了下mbedtls中使用隨機數(shù)的最終調(diào)用接口

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 !居然是標(biāo)準(zhǔn)C庫的rand函數(shù)!這!?。?/span>

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

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

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

4.5 知識點補缺

上面的思路,已經(jīng)將疑點對準(zhǔn)隨機數(shù)了,但是為了能準(zhǔn)確分析解決問題,我們需要將相關(guān)的理論知識惡補以下。

4.5.1 MQTT的心跳機制

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

  • keepalive參數(shù)

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

  • PINGREQ報文和PINGRESP報文

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

簡單總結(jié)下:

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

4.5.2 lwip協(xié)議棧

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

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

我這里重點提及下它使用隨機數(shù)的地方,關(guān)于它的初始化流程可以參見這里

在它的初始化流程中,需要執(zhí)行到一個tcp_init的函數(shù),位于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()); //關(guān)鍵操作:初始化的時候隨機取得tcp_port
  os_printf("tcp_port:%d
", tcp_port);
#endif /* LWIP_RANDOMIZE_INITIAL_LOCAL_PORTS && defined(LWIP_RAND) */
}

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

tcp_init無非是取得一個tcp_port的基準(zhǔn)偏移,后面在創(chuàng)建客戶端的時候,對服務(wù)器發(fā)起TCP鏈接,本地的端口號就是根據(jù)這個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:
  //關(guān)鍵操作: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的狀態(tài)圖

要熟練地分析上面的各個場景,務(wù)必需要對TCP的各個狀態(tài)非常了解。從網(wǎng)上找了一張關(guān)于TCP狀態(tài)介紹稍全的圖,供大家參考下:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

他的核心觀點就是:

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

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

結(jié)合我們抓的TCP報文,這不就是剛好驗證了我們的復(fù)現(xiàn)場景嗎?

4.5.4 TCP報文的標(biāo)志位

TCP的報文中規(guī)定有6種重要的標(biāo)志位:

  • URG:(Urgent Pointer field significant)緊急指針。用到的時候值為1,用來處理避免TCP數(shù)據(jù)流中斷?!具@個標(biāo)志位很少見】

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

  • PSH:(Push Function),PUSH標(biāo)志的數(shù)據(jù),置1時請求的數(shù)據(jù)段在接收方得到后就可直接送到應(yīng)用程序,而不必等到緩沖區(qū)滿時才傳送。

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

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

  • FIN:(No more data from sender)用來釋放連接,表明發(fā)送方已經(jīng)沒有數(shù)據(jù)發(fā)送了。

熟悉這幾個標(biāo)志位的基礎(chǔ)含義,基本上就可以看懂一段TCP網(wǎng)絡(luò)報文了。

4.6 深入分析:從理論分析到實戰(zhàn)分析

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

4.6.1 理論分析:理論上的復(fù)現(xiàn)路徑

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

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

他的博文中是分析了linux系統(tǒng)下的TCP協(xié)議對這種場景的報文回復(fù)情況,那么我試著從lwip協(xié)議棧的實現(xiàn)中,找找相關(guān)的處理是怎么樣的。

當(dāng)客戶端發(fā)起tcp connect的時候,調(diào)用的是lwip_connect,具體可以參考下面。

函數(shù)調(diào)用順序:-> 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;

  // 省略部分實現(xiàn)

  /* 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報文,通過底層的接口發(fā)送出去,同時將TCP的狀態(tài)切換到SYN_SENT狀態(tài)。

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

函數(shù)調(diào)用順序:-> 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"
", 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"
",
                                   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"
", (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;
}

函數(shù)比較長,我們抓重點,它這里就是根據(jù)當(dāng)前TCP的不同狀態(tài)做不同的處理。我們看到第80行,這里:

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

接著這個函數(shù),我們看下服務(wù)器端的處理,如果TCP鏈接已經(jīng)處于ESTABLISHED狀態(tài),當(dāng)它收到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)) {
        //在接收窗口內(nèi)的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
"));
      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"
",
       seqno, pcb->rcv_nxt));
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_process: unacceptable reset seqno %"U32_F" rcv_nxt %"U32_F"
",
       seqno, pcb->rcv_nxt));
      return ERR_OK;
    }
  }

  //當(dāng)服務(wù)器端收到一個處于ESTABLISHED狀態(tài)的連接收到一個SYN報文,就直接回復(fù)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;
  }

  //忽略部分代碼

結(jié)合下面的實際抓包,我們再仔細(xì)分析分析。

4.6.2 實戰(zhàn)分析:實戰(zhàn)中的場景路勁

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

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

  • 開機正常連上服務(wù)器,正常收到報文,PING包能發(fā)能收

706f5930-9fc7-11ec-952b-dac502259ad0.png

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

  • 設(shè)備重啟后,連接服務(wù)器,后面開始出現(xiàn)掉線

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

我們再仔細(xì)看下,這個時候,報文交互上發(fā)生了什么?

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

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

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

  • 觸發(fā)掉線重連機制,重新連上服務(wù)器

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

重連成功后,設(shè)備重新在線,PING包交互正常,恢復(fù)了。

4.6.3 解決疑惑:為何偶現(xiàn)而不必現(xiàn)

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

首先,在上面的分析中,我們得出一個很重要的結(jié)論,當(dāng)服務(wù)器端還處于連接狀態(tài)的TCP鏈接,收到一個由相同的四元組組成的SYN報文,最終就會觸發(fā)設(shè)備端產(chǎn)生RST報文,從而使得通訊鏈接發(fā)生“假鏈接”,影響實際通訊!

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

  • 相同的四元組構(gòu)成的SYN報文;

  • 前一個鏈接在TCP服務(wù)端還處于TCP狀態(tài)中的已鏈接狀態(tài)。

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

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

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

網(wǎng)絡(luò)通訊中隨機數(shù)不隨機引發(fā)的問題及解決方法

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

5 問題修復(fù)

當(dāng)一切的分析都站穩(wěn)了腳跟的時候,修復(fù)問題便是水到渠成的事情。

這里的修復(fù),其實主要是兩個方面:

5.1 解決鏈接標(biāo)準(zhǔn)C庫的rand函數(shù)的問題

在原生的lwip組件代碼中,隨機數(shù)的適配本身就是移植的一部分,很遺憾,在我們出問題的芯片上看到的隨機數(shù)還是原生態(tài)的rand函數(shù)。

要想解決掉這個問題,有兩個思路:

一個是直接在lwip組件中的arch.h里宏定義LWIP_RAND的地方直接切換成芯片的硬隨機數(shù)接口xxx_rand,這種是在預(yù)編譯階段就完成的;

還有一種就是在鏈接階段處理的,把原本鏈接rand函數(shù)直接替換鏈接成芯片平臺的硬隨機數(shù)接口xxx_rand,gcc編譯就一個這樣的鏈接選項支持這樣的功能。

為了最大化保持lwip組件的代碼完整,我們不想改它一行代碼,所以我們采用第二種解決思路,只需要在編譯構(gòu)建的全局鏈接參數(shù)中加上以下參數(shù)即可:

# enable hal-rand
GLOBAL_LDFLAGS += -Wl,--wrap=rand

同時在hal層的C文件中,添加實現(xiàn)一個叫__warp_rand的函數(shù)即可。

/* wrap hal TRNG function */
int __wrap_rand(void)
{    
    extern int xxx_trng_rand(void);    
    int ret = (int)xxx_trng_rand(); //call TRNG API    
    return ret;
}

經(jīng)以上修改之后,lwip組件中調(diào)用的rand函數(shù),最終就會調(diào)到xxx_trng_rand接口了,基本就解決了隨機數(shù)的問題。

5.2 解決芯片硬件隨機數(shù)不隨機的問題

但是,上面的分析部分,我們也提到了,這個芯片居然還出現(xiàn)了硬件隨機數(shù)不隨機的問題,準(zhǔn)確說,它獲取的不是隨機數(shù),而是一個無序的存儲序列,這無疑是一個重大bug。

就像這樣:

第一次開機:隨便獲取8個隨機數(shù),初步得到 12345 2345 3456 6789 5678 9867 234 567

第二次開機:隨便獲取8個隨機數(shù),還是得到 12345 2345 3456 6789 5678 9867 234 567

第三次開機:隨便獲取8個隨機數(shù),依然得到 12345 2345 3456 6789 5678 9867 234 567

這顯然是不隨機的,是會出問題的。由于我們沒有芯片的datasheet以及不能完全掌握其TRNG的工作原理,我們把問題拋給了原廠,幸運的是,原廠很快給我們打了個小patch。

這個小patch說簡單是真的簡單,就僅僅是加了一個延時;但是這個延時,代價有點大?。?!

以下是偽代碼,但足以展示這個代價的威力!

uint32_t xxx_trng_rand(void)
{    
    //enable TRNG register        
    
    //patch here    msleep(10);  //dealy 10ms        
    
    //read TRNG register data    
    uint32_t ret = read_TRNG();        
    //disable TRNG register        
    
    return ret;
}

what???每獲取一個隨機數(shù)都要延時10ms?那我在某次網(wǎng)絡(luò)通訊中,可能要獲取成百上千個隨機數(shù)???這積累的延時簡直不能接受啊!

按理說,芯片不能“弱”成這樣,也沒有這么不合理的設(shè)計!看它這個延時,無非的意思就是說,我的TRNG寄存器不是一上電就可以工作的,你得先給它預(yù)熱下,稍后再來取嘛。

OK,既然你是這樣的特性,那么我們可不可以在驅(qū)動初始化的時候就給你預(yù)熱呢?獲取隨機數(shù)的時候就不預(yù)熱了哇?

試試看,于是有了這樣的偽代碼:

void sys_driver_init(void)
{    
    //normal driver init        
    
    //special for TRNG warm up    
    //step1. enable TRNG register    
    
    //step2. msleep(10);  //dealy 10ms    
    
    //step3. disable TRNG register
}

uint32_t xxx_trng_rand(void)
{    
    //enable TRNG register        
    //read TRNG register data    
    uint32_t ret = read_TRNG();        
    //disable TRNG register       
    return ret;
}

這樣代碼一測試,完美!至少不需要每次獲取隨機數(shù)都dealy??!其實在寫這段熱身代碼的時候,也踩了些坑的,比如沒有先enable TRNG寄存器就去delay,這無疑是delay了個寂寞啊?

至少,所以需要修正的代碼已經(jīng)修正完成。值得注意的是,我們沒有改一行應(yīng)用層及組件層的代碼,那么修復(fù)后的情況究竟如何,下一章節(jié)我們來驗證驗證。

6 問題驗證

6.1 隨機數(shù)的問題驗證

這里的驗證,其實是要一層層來驗證,由于問題的根源在于隨機數(shù)的不隨機導(dǎo)致,那么我們有限要驗證的應(yīng)該是芯片TRNG的隨機性。

幸運的是,通過第5部分的patch代碼,我們有效地看到了TRNG的隨機性基本滿足了我們的要求,我們的驗證方法很簡單,就是開機完成初始化之后就獲取一組隨機數(shù),然后就重啟;不斷地測試,觀察1000左右的數(shù)據(jù)。

從1000的數(shù)據(jù),初步是可以看出去隨機性的,但如果需要過隨機數(shù)認(rèn)證的話,還得使用NIST專門的測試工具做更進(jìn)一步的驗證測試,這里就不展開論述了,有興趣的可以自行去了解下,像金融領(lǐng)域的PCI安全認(rèn)證,隨機數(shù)的測試是非常關(guān)鍵的一環(huán)。

6.2 偶發(fā)的網(wǎng)絡(luò)掉線問題驗證

這個驗證就要回到issue本身了,雖然我們在上面的分析階段,其實也做了部分邊分析邊修正邊驗證的工作,但上面的場景更加側(cè)重的是在不清楚穩(wěn)定的復(fù)現(xiàn)路徑的情況下不停地試錯。

所以,回歸驗證這個issue還是需要根據(jù)穩(wěn)定的復(fù)現(xiàn)路徑,做一些控制變量來單項驗證,比如每次重啟后就固定使用12345端口發(fā)起MQTT鏈接,觀察其復(fù)現(xiàn)情況;恢復(fù)正常修復(fù)后的隨機端口號,觀測其情況。

同時,還得把壓測環(huán)境搭建好,同步進(jìn)行壓力測試,觀測其情況。

只有以上幾點都通過驗證后,我們才有扎實的信心說:“這個issue可以close了”!

7 經(jīng)驗總結(jié)

  • 當(dāng)你對一個網(wǎng)絡(luò)問題靠邏輯思考解決不了的時候,第一要想到的方法就是抓包分析;

  • 抓包分析有方法,優(yōu)先排查上層網(wǎng)絡(luò)協(xié)議的報文,比如TCP/TLS等;當(dāng)上層協(xié)議包分析不出問題的時候,嘗試抓空口包;

  • TCP鏈接的狀態(tài)切換是所有基于TCP/IP協(xié)議的網(wǎng)絡(luò)通訊的基礎(chǔ),重點分析它有助于打開你的分析思路;

  • 所有偶現(xiàn)的問題,一定有復(fù)現(xiàn)路勁;如果你還沒出現(xiàn),僅僅是你的測試次數(shù)還不夠多;

  • 嵌入式里面的C庫,往往不是標(biāo)準(zhǔn)的,不能太輕易相對傳統(tǒng)意義上的C標(biāo)準(zhǔn)接口,多持懷疑的態(tài)度;

  • 隨機數(shù)的隨機性在網(wǎng)絡(luò)通訊中非常重要,當(dāng)你的網(wǎng)絡(luò)通訊超出你的想象的時候,不妨想想隨機數(shù)的可能性;

  • lwip協(xié)議棧的實現(xiàn)在嵌入式設(shè)備中太常用了,關(guān)于它的移植不能簡單的”拿來主義“,需要系統(tǒng)地、全面地了解其工作原理、代碼架構(gòu)和適配的基本工作,了解其可能出問題的點和常規(guī)的解決方法;

  • 技術(shù)偷懶使不得,技術(shù)債遲早都是要還的;

  • 疑難bug的解決要及時總結(jié)和復(fù)盤,形成一定的分析方法論,指不定哪天就幫助你解決其他相關(guān)的疑難問題。


原文標(biāo)題:【網(wǎng)絡(luò)通訊與網(wǎng)絡(luò)安全】網(wǎng)絡(luò)通訊中的隨機數(shù)如果不隨機會怎么樣!

文章出處:【微信公眾號:RTThread物聯(lián)網(wǎng)操作系統(tǒng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 網(wǎng)絡(luò)
    +關(guān)注

    關(guān)注

    14

    文章

    7586

    瀏覽量

    89007
  • wi-fi
    +關(guān)注

    關(guān)注

    14

    文章

    2157

    瀏覽量

    124715
  • 通訊
    +關(guān)注

    關(guān)注

    9

    文章

    911

    瀏覽量

    34985

原文標(biāo)題:【網(wǎng)絡(luò)通訊與網(wǎng)絡(luò)安全】網(wǎng)絡(luò)通訊中的隨機數(shù)如果不隨機會怎么樣!

文章出處:【微信號:RTThread,微信公眾號:RTThread物聯(lián)網(wǎng)操作系統(tǒng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    單片機C語言如何產(chǎn)生隨機數(shù)

    單片機C語言如何產(chǎn)生隨機數(shù) 隨機數(shù)在單片機的應(yīng)用也是很多的,當(dāng)然產(chǎn)生隨機數(shù)方法有很多,當(dāng)中有一個就是利用單片機定時器,取出未知的定時器T
    發(fā)表于 05-14 15:14

    產(chǎn)生隨機數(shù)方法有哪些

    隨機數(shù)在單片機的應(yīng)用也是很多的,當(dāng)然產(chǎn)生隨機數(shù)方法有很多,當(dāng)中有一個就是利用單片機定時器,取出未知的定時器THX和TLX的值,再加以運算得到一個規(guī)定范圍內(nèi)的
    發(fā)表于 07-15 09:08

    什么是隨機數(shù)

    做開發(fā)的工程師們應(yīng)該或多或少都接觸過隨機數(shù),可能認(rèn)為它就是一個隨機生成的數(shù)字嘛,使用時也很簡單,只要調(diào)用開發(fā)語言提供的函數(shù)即可。但實際上隨機數(shù)后面還是有著比較復(fù)雜但也有趣的知識點的。根據(jù)一般定義
    發(fā)表于 07-22 09:42

    基于FPGA的隨機數(shù)性能檢測設(shè)計

    為了滿足對隨機數(shù)性能有一定要求的系統(tǒng)能夠?qū)崟r檢測隨機數(shù)性能的需求,提出了一種基于FPGA的隨機數(shù)性能檢測設(shè)計方案。根據(jù)NIST的測試標(biāo)準(zhǔn),采用基于統(tǒng)計的方法,在FPGA內(nèi)部實現(xiàn)了
    發(fā)表于 07-24 16:52 ?45次下載
    基于FPGA的<b class='flag-5'>隨機數(shù)</b>性能檢測設(shè)計

    產(chǎn)生隨機數(shù)

    一個自己寫的產(chǎn)生隨機數(shù)的工程
    發(fā)表于 12-01 15:45 ?13次下載

    神經(jīng)網(wǎng)絡(luò)的偽隨機數(shù)生成方法

    為了克服有限精度效應(yīng)對混沌系統(tǒng)的退化影響,改善所生成隨機序列的統(tǒng)計性能,設(shè)計了一種新的基于六維CNN(細(xì)胞神經(jīng)網(wǎng)絡(luò))的64 bit偽隨機數(shù)生成方法。在該
    發(fā)表于 02-02 15:49 ?0次下載

    隨機數(shù)生成算法

    在計算機上用數(shù)學(xué)的方法產(chǎn)生隨機數(shù)列是目前通用的方法,它的特點是占用的內(nèi)存少,速度快.用數(shù)學(xué)方法產(chǎn)生的隨機數(shù)列是根據(jù)確定的算法推算出來的,嚴(yán)格
    發(fā)表于 04-03 10:25 ?6次下載

    如何在C語言中使用隨機數(shù)

    通常情況下,使用最多的方法的就是使用rand函數(shù)隨機生成偽隨機數(shù)來完成隨機數(shù)的生成工作。注意這里的偽隨機數(shù)并非是假的! 只不過是計算機按自己
    的頭像 發(fā)表于 11-09 16:46 ?5195次閱讀

    單片機產(chǎn)生隨機數(shù)方法

    隨機數(shù)在單片機的應(yīng)用也是很多的,當(dāng)然產(chǎn)生隨機數(shù)方法有很多,當(dāng)中有一個就是利用單片機定時器,取出未知的定時器THX和TLX的值,再加以運算得到一個規(guī)定范圍內(nèi)的
    發(fā)表于 02-23 10:37 ?2.2w次閱讀

    單片機產(chǎn)生隨機數(shù)的兩種方法

    隨機數(shù)在單片機的應(yīng)用也是很多的,當(dāng)然產(chǎn)生隨機數(shù)方法有很多,當(dāng)中有一個就是利用單片機定時器,取出未知的定時器THX和TLX的值,再加以運算得到一個規(guī)定范圍內(nèi)的
    發(fā)表于 03-01 11:04 ?2309次閱讀

    DApp的隨機數(shù)為什么會被黑客破解

    隨機數(shù)可以分為真隨機數(shù)和偽隨機數(shù)。真隨機數(shù)需要同時滿足隨機性、不可預(yù)測性、不可重現(xiàn)性,而偽隨機數(shù)
    發(fā)表于 10-18 10:59 ?2497次閱讀

    Python隨機數(shù)模塊的隨機函數(shù)使用

    隨機數(shù)在日常的應(yīng)用開發(fā),使用的比較多,比如抽獎游戲,如果你不依靠隨機數(shù),就會變的由規(guī)律,容易被人發(fā)現(xiàn)規(guī)律。比如我們的斗地主游戲,它的發(fā)牌程序也會隨機給每個人發(fā)牌,還有一些加密使用的也
    的頭像 發(fā)表于 01-18 17:55 ?2408次閱讀
    Python<b class='flag-5'>隨機數(shù)</b>模塊的<b class='flag-5'>隨機</b>函數(shù)使用

    如何利用SystemVerilog仿真生成隨機數(shù)

    采用SystemVerilog進(jìn)行仿真則更容易生成隨機數(shù),而且對隨機數(shù)具有更強的可控性。對于隨機變量,在SystemVerilog可通過rand或randc加數(shù)據(jù)類型的方式定義。ra
    的頭像 發(fā)表于 10-30 10:33 ?1.1w次閱讀
    如何利用SystemVerilog仿真生成<b class='flag-5'>隨機數(shù)</b>

    單片機C語言如何產(chǎn)生隨機數(shù)

    隨機數(shù)在單片機的應(yīng)用也是很多的,當(dāng)然產(chǎn)生隨機數(shù)方法有很多,當(dāng)中有一個就是利用單片機定時器,取出未知的定時器THX和TLX的值,再加以運算得到一個規(guī)定范圍內(nèi)的
    發(fā)表于 02-08 17:12 ?11次下載
    單片機C語言如何產(chǎn)生<b class='flag-5'>隨機數(shù)</b>

    FPGA的偽隨機數(shù)發(fā)生器學(xué)習(xí)介紹

    隨機試驗的結(jié)果,產(chǎn)生隨機數(shù)有多種不同的方法。這些方法被稱為隨機數(shù)生成器。隨機數(shù)最重要的特性是它
    的頭像 發(fā)表于 09-12 09:13 ?1631次閱讀