在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

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

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

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

網(wǎng)絡(luò)通訊中隨機(jī)數(shù)不隨機(jī)引發(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 寫在前言

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

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

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

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

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

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

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

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

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

2 問題描述

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

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

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

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

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

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

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

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

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

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

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

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

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

3 場景再現(xiàn)

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

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

說句不好聽的:寫軟件的,誰還沒幾個(gè)解決不了的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é)點(diǎn)下,MQTT的ping包丟了,所以在搭建復(fù)現(xiàn)環(huán)境的時(shí)候,我們嘗試了修改MQTT ping包的發(fā)送周期。我們提測的版本用的是典型值60S,所以復(fù)測中我們同步修改2個(gè)版本,分別是30S120S

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

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

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

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

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

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

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

4 問題分析

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

其次,排查軟件問題,無非兩個(gè)方向,要不從大到小,要么從小到大從大到小指的是先從宏觀的軟件架構(gòu)層面去思考和分析,層層剝離,循序漸進(jìn),直到分析可能出現(xiàn)的更小范圍,各個(gè)排查突破;從小到大指的是從微觀的末端錯(cuò)誤log開始分析,一步步反推導(dǎo)致這個(gè)錯(cuò)誤的出現(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ò)通訊中隨機(jī)數(shù)不隨機(jī)引發(fā)的問題及解決方法

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

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

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

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

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

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

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

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

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

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

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

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

就像這樣:

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

4.3 要放大招:三板斧出擊

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

4.3.1 第一板斧:MQTT log

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

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

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

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

還有一點(diǎn),我們使用的MQTT實(shí)現(xiàn)包中對MQTT收到的報(bào)文,全部都是在mqtt_yield(Client, timeout_ms)查詢式接收,當(dāng)收到一個(gè)有效的MQTT報(bào)文,會(huì)有類似下面一段的處理代碼:

    // 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ù)的,一定會(huì)有"receive ping response!"的log輸出,這也是斷定MQTT是否掉線的一個(gè)簡單判斷。

4.3.2 第三板斧:TCP/IP抓包

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

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

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

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

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

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

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

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

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

wireshark中對報(bào)文的過濾操作,如下圖所示:

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

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

如下所示:

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

4.3.3 第二板斧:空口抓包

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

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

它的抓包界面長這樣:

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

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

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

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

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

4.3.4 分析小結(jié)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

也確認(rèn)了下mbedtls中使用隨機(jī)數(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ù)!這!!!

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

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

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

4.5 知識點(diǎn)補(bǔ)缺

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

4.5.1 MQTT的心跳機(jī)制

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

  • keepalive參數(shù)

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

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

  • PINGREQ報(bào)文和PINGRESP報(bào)文

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

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

簡單總結(jié)下:

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

4.5.2 lwip協(xié)議棧

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

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

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

在它的初始化流程中,需要執(zhí)行到一個(gè)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)鍵操作:初始化的時(shí)候隨機(jī)取得tcp_port
  os_printf("tcp_port:%d
", tcp_port);
#endif /* LWIP_RANDOMIZE_INITIAL_LOCAL_PORTS && defined(LWIP_RAND) */
}

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

tcp_init無非是取得一個(gè)tcp_port的基準(zhǔn)偏移,后面在創(chuàng)建客戶端的時(shí)候,對服務(wù)器發(fā)起TCP鏈接,本地的端口號就是根據(jù)這個(gè)tcp_port來計(jì)算出來的,代碼如下:

//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鏈接的端口號,因?yàn)閠cp_port兩次(很有可能)是一樣的。

4.5.3 TCP的狀態(tài)圖

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

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

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

他的核心觀點(diǎn)就是:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

當(dāng)客戶端發(fā)起tcp connect的時(shí)候,調(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;

  // 省略部分實(shí)現(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在組一個(gè)帶有SYN的TCP報(bào)文,通過底層的接口發(fā)送出去,同時(shí)將TCP的狀態(tài)切換到SYN_SENT狀態(tài)。

由于我們實(shí)現(xiàn)的lwip是異步模式,所以最終接收對方的響應(yīng)報(bào)文在tcp_in.c里面,我們注意到有這么一個(gè)函數(shù)tcp_process,它就是TCP狀態(tài)的狀態(tài)機(jī)實(shí)現(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ù)比較長,我們抓重點(diǎn),它這里就是根據(jù)當(dāng)前TCP的不同狀態(tài)做不同的處理。我們看到第80行,這里:

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

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

接著這個(gè)函數(shù),我們看下服務(wù)器端的處理,如果TCP鏈接已經(jīng)處于ESTABLISHED狀態(tài),當(dāng)它收到SYN報(bào)文時(shí),它會(huì)怎么處理呢?

/**
 * 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報(bào)文,最終是在這里處理!!!
        /* 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ù)器端收到一個(gè)處于ESTABLISHED狀態(tài)的連接收到一個(gè)SYN報(bào)文,就直接回復(fù)ACK報(bào)文了。
  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é)合下面的實(shí)際抓包,我們再仔細(xì)分析分析。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

看著三次握手多順利,同時(shí)我們留意到這次的客戶端端口不再是26947了,而是一個(gè)新的端口號26946;這是因?yàn)樽グ绞降脑颍@個(gè)端口號并不完全體現(xiàn)是設(shè)備端lwip的tcp_port,但至少是能反映它是在變化的。

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

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

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

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

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

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

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

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

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

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

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

而異常的場景下,報(bào)文如下:

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

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

5 問題修復(fù)

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

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

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

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

要想解決掉這個(gè)問題,有兩個(gè)思路:

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

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

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

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

同時(shí)在hal層的C文件中,添加實(shí)現(xiàn)一個(gè)叫__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ù),最終就會(huì)調(diào)到xxx_trng_rand接口了,基本就解決了隨機(jī)數(shù)的問題。

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

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

就像這樣:

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

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

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

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

這個(gè)小patch說簡單是真的簡單,就僅僅是加了一個(gè)延時(shí);但是這個(gè)延時(shí),代價(jià)有點(diǎn)大!!!

以下是偽代碼,但足以展示這個(gè)代價(jià)的威力!

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???每獲取一個(gè)隨機(jī)數(shù)都要延時(shí)10ms?那我在某次網(wǎng)絡(luò)通訊中,可能要獲取成百上千個(gè)隨機(jī)數(shù)啊?這積累的延時(shí)簡直不能接受啊!

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

OK,既然你是這樣的特性,那么我們可不可以在驅(qū)動(dòng)初始化的時(shí)候就給你預(yù)熱呢?獲取隨機(jī)數(shù)的時(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;
}

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

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

6 問題驗(yàn)證

6.1 隨機(jī)數(shù)的問題驗(yàn)證

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

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

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

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

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

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

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

只有以上幾點(diǎn)都通過驗(yàn)證后,我們才有扎實(shí)的信心說:“這個(gè)issue可以close了”!

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

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

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

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

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

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

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

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

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

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


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

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

審核編輯:湯梓紅

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

    關(guān)注

    14

    文章

    7586

    瀏覽量

    89005
  • 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ò)通訊中的隨機(jī)數(shù)如果不隨機(jī)會(huì)怎么樣!

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

收藏 人收藏

    評論

    相關(guān)推薦

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

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

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

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

    什么是隨機(jī)數(shù)

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

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

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

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

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

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

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

    隨機(jī)數(shù)生成算法

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    隨機(jī)試驗(yàn)的結(jié)果,產(chǎn)生隨機(jī)數(shù)有多種不同的方法。這些方法被稱為隨機(jī)數(shù)生成器。隨機(jī)數(shù)最重要的特性是它
    的頭像 發(fā)表于 09-12 09:13 ?1631次閱讀
    主站蜘蛛池模板: 国产精品夜夜春夜夜| 免费观看视频高清www| 一本高清在线| 天天在线天天在线天天影视| 天天亚洲综合| 人人看人人做| 国产骚b| 性xxxfreexxxx性欧美| 一级毛片西西人体44rt高清| 老司机亚洲精品影院在线观看| 中文天堂在线最新版在线www| 亚洲精品美女| 色激情网| 久久99精品久久久久久园产越南| 国产三级精品在线| 亚洲第一视频网| 黑人黄色片| 一区二区福利| 亚洲综合成人网在线观看| 午夜精品久久久久久久久| 午夜国产理论| 国产亚洲精品aa在线看| 婷婷丁香五月中文字幕| 免费大片黄在线观看| 亚洲成在人| 欧美三级黄视频| a级毛毛片看久久| 日日搞夜夜操| 全亚洲最大的777io影院| 黄蓉h肉辣文大全| 色午夜视频| 狠狠躁夜夜躁人人爽天天天天| 又粗又爽又色男女乱淫播放男女| 手机看日韩毛片福利盒子| 免费看h的网站| 亚洲三级免费| 女人张开腿让男人桶视频免费大全| 国产综合图片| 免费国产综合视频在线看| 中文在线 | 中文| 天堂电影在线|