動(dòng)圖圖解!收到RST,就一定會(huì)斷開(kāi)TCP連接嗎?
想必大家已經(jīng)知道我的niao性,搞個(gè)標(biāo)題,就是不喜歡立馬回答。
就是要搞一大堆原理性的東西,再回答標(biāo)題的問(wèn)題。
說(shuō)這個(gè)是因?yàn)槲疫@次會(huì)把問(wèn)題的答案就放到開(kāi)頭嗎?
不!
我就不!
但是大家可以直接根據(jù)目錄看自己感興趣的部分。
之所以要先鋪墊一些原理,還是希望大家能先看些基礎(chǔ)的,再慢慢循序漸進(jìn),這樣有利于建立知識(shí)體系。多一點(diǎn)上下文,少一點(diǎn)gap。
好了,進(jìn)入正題。
下面是這篇文章的目錄。

什么是RST
我們都知道TCP正常情況下斷開(kāi)連接是用四次揮手,那是正常時(shí)候的優(yōu)雅做法。
但異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個(gè)機(jī)制去強(qiáng)行關(guān)閉連接。
RST 就是用于這種情況,一般用來(lái)異常地關(guān)閉一個(gè)連接。它是一個(gè)TCP包頭中的標(biāo)志位。
正常情況下,不管是發(fā)出,還是收到置了這個(gè)標(biāo)志位的數(shù)據(jù)包,相應(yīng)的內(nèi)存、端口等連接資源都會(huì)被釋放。從效果上來(lái)看就是TCP連接被關(guān)閉了。
而接收到 RST的一方,一般會(huì)看到一個(gè) connection reset 或 ?connection refused 的報(bào)錯(cuò)。

怎么知道收到RST了?
我們知道內(nèi)核跟應(yīng)用層是分開(kāi)的兩層,網(wǎng)絡(luò)通信功能在內(nèi)核,我們的客戶(hù)端或服務(wù)端屬于應(yīng)用層。應(yīng)用層只能通過(guò) send/recv 與內(nèi)核交互,才能感知到內(nèi)核是不是收到了RST。
當(dāng)本端收到遠(yuǎn)端發(fā)來(lái)的RST后,內(nèi)核已經(jīng)認(rèn)為此鏈接已經(jīng)關(guān)閉。
此時(shí)如果本端應(yīng)用層嘗試去執(zhí)行 讀數(shù)據(jù)操作,比如recv,應(yīng)用層就會(huì)收到 Connection reset by peer 的報(bào)錯(cuò),意思是遠(yuǎn)端已經(jīng)關(guān)閉連接。

如果本端應(yīng)用層嘗試去執(zhí)行寫(xiě)數(shù)據(jù)操作,比如send,那么應(yīng)用層就會(huì)收到 Broken pipe 的報(bào)錯(cuò),意思是發(fā)送通道已經(jīng)壞了。

這兩個(gè)是開(kāi)發(fā)過(guò)程中很經(jīng)常遇到的報(bào)錯(cuò),感覺(jué)大家可以把這篇文章放進(jìn)收藏夾吃灰了,等遇到這個(gè)問(wèn)題了,再打開(kāi)來(lái)擦擦灰,說(shuō)不定對(duì)你會(huì)有幫助。
出現(xiàn)RST的場(chǎng)景有哪些
RST一般出現(xiàn)于異常情況,歸類(lèi)為 對(duì)端的端口不可用 和 socket提前關(guān)閉。
端口不可用
端口不可用分為兩種情況。要么是這個(gè)端口從來(lái)就沒(méi)有"可用"過(guò),比如根本就沒(méi)監(jiān)聽(tīng)(listen)過(guò);要么就是曾經(jīng)"可用",但現(xiàn)在"不可用"了,比如服務(wù)突然崩了。
端口未監(jiān)聽(tīng)

服務(wù)端listen 方法會(huì)創(chuàng)建一個(gè)sock放入到全局的哈希表中。
此時(shí)客戶(hù)端發(fā)起一個(gè)connect請(qǐng)求到服務(wù)端。服務(wù)端在收到數(shù)據(jù)包之后,第一時(shí)間會(huì)根據(jù)IP和端口從哈希表里去獲取sock。

如果服務(wù)端執(zhí)行過(guò)listen,就能從全局哈希表里拿到sock。
但如果服務(wù)端沒(méi)有執(zhí)行過(guò)listen,那哈希表里也就不會(huì)有對(duì)應(yīng)的sock,結(jié)果當(dāng)然是拿不到。此時(shí),正常情況下服務(wù)端會(huì)發(fā)RST給客戶(hù)端。
端口未監(jiān)聽(tīng)就一定會(huì)發(fā)RST嗎?
不一定。上面提到,發(fā)RST的前提是正常情況下,我們看下源碼。
//?net/ipv4/tcp_ipv4.c??
//?代碼經(jīng)過(guò)刪減
int?tcp_v4_rcv(struct?sk_buff?*skb)
{
????//?根據(jù)ip、端口等信息?獲取sock。
????sk?=?__inet_lookup_skb(&tcp_hashinfo,?skb,?th->source,?th->dest);
????if?(!sk)
????????goto?no_tcp_socket;
no_tcp_socket:
????//?檢查數(shù)據(jù)包有沒(méi)有出錯(cuò)
????if?(skb->len?(th->doff?<2)?||?tcp_checksum_complete(skb))?{
????????//?錯(cuò)誤記錄
????}?else?{
????????//?發(fā)送RST
????????tcp_v4_send_reset(NULL,?skb);
????}
}
內(nèi)核在收到數(shù)據(jù)后會(huì)從物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、應(yīng)用層,一層一層往上傳遞。到傳輸層的時(shí)候,根據(jù)當(dāng)前數(shù)據(jù)包的協(xié)議是TCP還是UDP走不一樣的函數(shù)方法??梢院?jiǎn)單認(rèn)為,TCP數(shù)據(jù)包都會(huì)走到 tcp_v4_rcv()。這個(gè)方法會(huì)從全局哈希表里獲取 sock,如果此時(shí)服務(wù)端沒(méi)有listen()過(guò) , 那肯定獲取不了sock,會(huì)跳轉(zhuǎn)到no_tcp_socket的邏輯。
注意這里會(huì)先走一個(gè) tcp_checksum_complete(),目的是看看數(shù)據(jù)包的校驗(yàn)和(Checksum)是否合法。
校驗(yàn)和可以驗(yàn)證數(shù)據(jù)從端到端的傳輸中是否出現(xiàn)異常。由發(fā)送端計(jì)算,然后由接收端驗(yàn)證。計(jì)算范圍覆蓋數(shù)據(jù)包里的TCP首部和TCP數(shù)據(jù)。
如果在發(fā)送端到接收端傳輸過(guò)程中,數(shù)據(jù)發(fā)生任何改動(dòng),比如被第三方篡改,那么接收方能檢測(cè)到校驗(yàn)和有差錯(cuò),此時(shí)TCP段會(huì)被直接丟棄。如果校驗(yàn)和沒(méi)問(wèn)題,那才會(huì)發(fā)RST。
所以,只有在數(shù)據(jù)包沒(méi)問(wèn)題的情況下,比如校驗(yàn)和沒(méi)問(wèn)題,才會(huì)發(fā)RST包給對(duì)端。
為什么數(shù)據(jù)包異常的情況下,不發(fā)RST?
一個(gè)數(shù)據(jù)包連校驗(yàn)都不能通過(guò),那這個(gè)包,多半有問(wèn)題。

有可能是在發(fā)送的過(guò)程中被篡改了,又或者,可能只是一個(gè)胡亂偽造的數(shù)據(jù)包。
五層網(wǎng)絡(luò),不管是哪一層,只要遇到了這種數(shù)據(jù),推薦的做法都是默默扔掉,而不是去回復(fù)一個(gè)消息告訴對(duì)方數(shù)據(jù)有問(wèn)題。
如果對(duì)方用的是TCP,是可靠傳輸協(xié)議,發(fā)現(xiàn)很久沒(méi)有ACK響應(yīng),自己就會(huì)重傳。
如果對(duì)方用的是UDP,說(shuō)明發(fā)送端已經(jīng)接受了“不可靠會(huì)丟包”的事實(shí),那丟了就丟了。
因此,數(shù)據(jù)包異常的情況下,默默扔掉,不發(fā)RST,非常合理。

還是不能理解?那我再舉個(gè)例子。
正常人噴你,他說(shuō)話(huà)條理清晰,主謂賓分明。此時(shí)你噴回去,那你是個(gè)充滿(mǎn)熱情,正直,富有判斷力的好人。
而此時(shí)一個(gè)憨憨也想噴你,但他思維混亂,連話(huà)都說(shuō)不清楚,一直阿巴阿巴的,你雖然聽(tīng)不懂,但大受震撼,此時(shí)你會(huì)?
A:跟他激情互噴
B:不跟他一般見(jiàn)識(shí),就當(dāng)沒(méi)聽(tīng)過(guò)
一般來(lái)說(shuō)最優(yōu)選擇是B,畢竟你理他,他反而來(lái)勁。
這下,應(yīng)該就懂了。
程序啟動(dòng)了但是崩了
端口不可用的場(chǎng)景里,除了端口未監(jiān)聽(tīng)以外,還有可能是從前監(jiān)聽(tīng)了,但服務(wù)端機(jī)器上做監(jiān)聽(tīng)操作的應(yīng)用程序突然崩了,此時(shí)客戶(hù)端還像往常一樣正常發(fā)送消息,服務(wù)器內(nèi)核協(xié)議棧收到消息后,則會(huì)回一個(gè)RST。在開(kāi)發(fā)過(guò)程中,這種情況是最常見(jiàn)的。
比如你的服務(wù)端應(yīng)用程序里,弄了個(gè)空指針,或者數(shù)組越界啥的,程序立馬就崩了。

這種情況跟端口未監(jiān)聽(tīng)本質(zhì)上類(lèi)似,在服務(wù)端的應(yīng)用程序崩潰后,原來(lái)監(jiān)聽(tīng)的端口資源就被釋放了,從效果上來(lái)看,類(lèi)似于處于CLOSED狀態(tài)。
此時(shí)服務(wù)端又收到了客戶(hù)端發(fā)來(lái)的消息,內(nèi)核協(xié)議棧會(huì)根據(jù)IP端口,從全局哈希表里查找sock,結(jié)果當(dāng)然是拿不到對(duì)應(yīng)的sock數(shù)據(jù),于是走了跟上面"端口未監(jiān)聽(tīng)"時(shí)一樣的邏輯,回了個(gè)RST??蛻?hù)端在收到RST后也釋放了sock資源,從效果上來(lái)看,就是連接斷了。
RST和502的關(guān)系
上面這張圖,服務(wù)端程序崩潰后,如果客戶(hù)端再有數(shù)據(jù)發(fā)送,會(huì)出現(xiàn)RST。但如果在客戶(hù)端和服務(wù)端中間再加一個(gè)nginx,就像下圖一樣。

nginx會(huì)作為客戶(hù)端和服務(wù)端之間的"中間人角色",負(fù)責(zé)轉(zhuǎn)發(fā)請(qǐng)求和響應(yīng)結(jié)果。但當(dāng)服務(wù)端程序崩潰,比如出現(xiàn)野指針或者OOM的問(wèn)題,那轉(zhuǎn)發(fā)到服務(wù)器的請(qǐng)求,必然得不到響應(yīng),后端服務(wù)端還會(huì)返回一個(gè)RST給nginx。nginx在收到這個(gè)RST后會(huì)斷開(kāi)與服務(wù)端的連接,同時(shí)返回客戶(hù)端一個(gè)502錯(cuò)誤碼。
所以,出現(xiàn)502問(wèn)題,一般情況下都是因?yàn)楹蠖顺绦虮懒?,基于這一點(diǎn)假設(shè),去看看監(jiān)控是不是發(fā)生了OOM或者日志是否有空指針等報(bào)錯(cuò)信息。
socket提前關(guān)閉
這種情況分為本端提前關(guān)閉,和遠(yuǎn)端提前關(guān)閉。
本端提前關(guān)閉
如果本端socket接收緩沖區(qū)還有數(shù)據(jù)未讀,此時(shí)提前close() socket。那么本端會(huì)先把接收緩沖區(qū)的數(shù)據(jù)清空,然后給遠(yuǎn)端發(fā)一個(gè)RST。

遠(yuǎn)端提前關(guān)閉
遠(yuǎn)端已經(jīng)close()了socket,此時(shí)本端還嘗試發(fā)數(shù)據(jù)給遠(yuǎn)端。那么遠(yuǎn)端就會(huì)回一個(gè)RST。

大家知道,TCP是全雙工通信,意思是發(fā)送數(shù)據(jù)的同時(shí),還可以接收數(shù)據(jù)。
Close()的含義是,此時(shí)要同時(shí)關(guān)閉發(fā)送和接收消息的功能。
客戶(hù)端執(zhí)行close(), 正常情況下,會(huì)發(fā)出第一次揮手FIN,然后服務(wù)端回第二次揮手ACK。如果在第二次和第三次揮手之間,如果服務(wù)方還嘗試傳數(shù)據(jù)給客戶(hù)端,那么客戶(hù)端不僅不收這個(gè)消息,還會(huì)發(fā)一個(gè)RST消息到服務(wù)端。直接結(jié)束掉這次連接。
對(duì)方?jīng)]收到RST,會(huì)怎么樣?
我們知道TCP是可靠傳輸,意味著本端發(fā)一個(gè)數(shù)據(jù),遠(yuǎn)端在收到這個(gè)數(shù)據(jù)后就會(huì)回一個(gè)ACK,意思是"我收到這個(gè)包了"。
而RST,不需要ACK確認(rèn)包。
因?yàn)?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(255, 82, 82);background: rgb(248, 248, 248);">RST本來(lái)就是設(shè)計(jì)來(lái)處理異常情況的,既然都已經(jīng)在異常情況下了,還指望對(duì)方能正常回你一個(gè)ACK嗎?可以幻想,不要妄想。
但問(wèn)題又來(lái)了,網(wǎng)絡(luò)環(huán)境這么復(fù)雜,丟包也是分分鐘的事情,既然RST包不需要ACK來(lái)確認(rèn),那萬(wàn)一對(duì)方就是沒(méi)收到RST,會(huì)怎么樣?

RST丟了,問(wèn)題不大。比方說(shuō)上圖服務(wù)端,發(fā)了RST之后,服務(wù)端就認(rèn)為連接不可用了。
如果客戶(hù)端之前發(fā)送了數(shù)據(jù),一直沒(méi)等到這個(gè)數(shù)據(jù)的確認(rèn)ACK,就會(huì)重發(fā),重發(fā)的時(shí)候,自然就會(huì)觸發(fā)一個(gè)新的RST包。
而如果客戶(hù)端之前沒(méi)有發(fā)數(shù)據(jù),但服務(wù)端的RST丟了,TCP有個(gè)keepalive機(jī)制,會(huì)定期發(fā)送探活包,這種數(shù)據(jù)包到了服務(wù)端,也會(huì)重新觸發(fā)一個(gè)RST。

收到RST就一定會(huì)斷開(kāi)連接嗎?
先說(shuō)結(jié)論,不一定會(huì)斷開(kāi)。我們看下源碼。
//?net/ipv4/tcp_input.c
static?bool?tcp_validate_incoming()
{
????//?獲取sock
????struct?tcp_sock?*tp?=?tcp_sk(sk);
????// step 1:先判斷seq是否合法(是否在合法接收窗口范圍內(nèi))
????if?(!tcp_sequence(tp,?TCP_SKB_CB(skb)->seq,?TCP_SKB_CB(skb)->end_seq))?{
????????goto?discard;
????}
????// step 2:執(zhí)行收到 RST 后該干的事情
????if?(th->rst)?{
????????if?(TCP_SKB_CB(skb)->seq?==?tp->rcv_nxt)
????????????tcp_reset(sk);
????????else
????????????tcp_send_challenge_ack(sk);
????????goto?discard;
????}
}
收到RST包,第一步會(huì)通過(guò)tcp_sequence先看下這個(gè)seq是否合法,其實(shí)主要是看下這個(gè)seq是否在合法接收窗口范圍內(nèi)。如果不在范圍內(nèi),這個(gè)RST包就會(huì)被丟棄。
至于接收窗口是個(gè)啥,我們先看下面這個(gè)圖。

這里黃色的部分,就是指接收窗口,只要RST包的seq不在這個(gè)窗口范圍內(nèi),那就會(huì)被丟棄。
為什么要校驗(yàn)是否在窗口范圍內(nèi)
正常情況下客戶(hù)端服務(wù)端雙方可以通過(guò)RST來(lái)斷開(kāi)連接。假設(shè)不做seq校驗(yàn),如果這時(shí)候有不懷好意的第三方介入,構(gòu)造了一個(gè)RST包,且在TCP和IP等報(bào)頭都填上客戶(hù)端的信息,發(fā)到服務(wù)端,那么服務(wù)端就會(huì)斷開(kāi)這個(gè)連接。同理也可以偽造服務(wù)端的包發(fā)給客戶(hù)端。這就叫RST攻擊。

受到RST攻擊時(shí),從現(xiàn)象上看,客戶(hù)端老感覺(jué)服務(wù)端崩了,這非常影響用戶(hù)體驗(yàn)。
如果這是個(gè)游戲,我相信多崩幾次,第二天大家就不來(lái)玩了。
實(shí)際消息發(fā)送過(guò)程中,接收窗口是不斷移動(dòng)的,seq也是在飛快的變動(dòng)中,此時(shí)第三方是比較難構(gòu)造出合法seq的RST包的,那么通過(guò)這個(gè)seq校驗(yàn),就可以攔下了很多不合法的消息。
加了窗口校驗(yàn)就不能用RST攻擊了嗎
不是,只是增加了攻擊的成本。但如果想搞,還是可搞的。
以下是面向監(jiān)獄編程的環(huán)節(jié)。
希望大家只了解原理就好了,不建議使用。
相信大家都不喜歡穿著藍(lán)白條紋的衣服,拍純獄風(fēng)的照片。
從上面可以知道,不是每一個(gè)RST包都會(huì)導(dǎo)致連接重置的,要求是這個(gè)RST包的seq要在窗口范圍內(nèi),所以,問(wèn)題就變成了,我們?cè)趺礃硬拍軜?gòu)造出合法的seq。
盲猜seq
窗口數(shù)值seq本質(zhì)上只是個(gè)uint32類(lèi)型。
struct?tcp_skb_cb?{
????__u32???????seq;????????/*?Starting?sequence?number?*/
}
如果在這個(gè)范圍內(nèi)瘋狂猜測(cè)seq數(shù)值,并構(gòu)造對(duì)應(yīng)的包,發(fā)到目的機(jī)器,雖然概率低,但是總是能被試出來(lái),從而實(shí)現(xiàn)RST攻擊。這種亂棍打死老師傅的方式,就是所謂的合法窗口盲打(blind in-window attacks)。
覺(jué)得這種方式比較笨?那有沒(méi)有聰明點(diǎn)的方式,還真有,但是在這之前需要先看下面的這個(gè)問(wèn)題。
已連接狀態(tài)下收到第一次握手包會(huì)怎么樣?
我們需要了解一個(gè)問(wèn)題,比如服務(wù)端在已連接(ESTABLISHED)狀態(tài)下,如果收到客戶(hù)端發(fā)來(lái)的第一次握手包(SYN),會(huì)怎么樣?
以前我以為服務(wù)單會(huì)認(rèn)為客戶(hù)端憨憨了,直接RST連接。
但實(shí)際,并不是。
static?bool?tcp_validate_incoming()
{
????struct?tcp_sock?*tp?=?tcp_sk(sk);
????/*?判斷seq是否在合法窗口內(nèi)?*/
????if?(!tcp_sequence(tp,?TCP_SKB_CB(skb)->seq,?TCP_SKB_CB(skb)->end_seq))?{
????????if?(!th->rst)?{
????????????//?收到一個(gè)不在合法窗口內(nèi)的SYN包
????????????if?(th->syn)
????????????????goto?syn_challenge;
????????}
????}
????/*?
?????*?RFC?5691?4.2?:?發(fā)送?challenge?ack
?????*/
????if?(th->syn)?{
syn_challenge:
????????tcp_send_challenge_ack(sk);
????}
}
當(dāng)客戶(hù)端發(fā)出一個(gè)不在合法窗口內(nèi)的SYN包的時(shí)候,服務(wù)端會(huì)發(fā)一個(gè)帶有正確的seq數(shù)據(jù)ACK包出來(lái),這個(gè)ACK包叫 challenge ack。

上圖是抓包的結(jié)果,用scapy隨便偽造一個(gè)seq=5的包發(fā)到服務(wù)端(端口9090),服務(wù)端回復(fù)一個(gè)帶有正確seq值的challenge ack包給客戶(hù)端(端口8888)。
利用challenge ack獲取seq
上面提到的這個(gè)challenge ack ,仿佛為盲猜seq的老哥們打開(kāi)了一個(gè)新世界。
在獲得這個(gè)challenge ack后,攻擊程序就可以以ack值為基礎(chǔ),在一定范圍內(nèi)設(shè)置seq,這樣造成RST攻擊的幾率就大大增加了。

總結(jié)
RST其實(shí)是TCP包頭里的一個(gè)標(biāo)志位,目的是為了在異常情況下關(guān)閉連接。
內(nèi)核收到RST后,應(yīng)用層只能通過(guò)調(diào)用讀/寫(xiě)操作來(lái)感知,此時(shí)會(huì)對(duì)應(yīng)獲得 Connection reset by peer 和Broken pipe 報(bào)錯(cuò)。
發(fā)出RST后不需要得到對(duì)方的ACK確認(rèn)包,因此RST丟失后對(duì)方不能立刻感知,但是通過(guò)下一次重傳數(shù)據(jù)或keepalive心跳包可以導(dǎo)致RST重傳。
收到RST包,不一定會(huì)斷開(kāi)連接,seq不在合法窗口范圍內(nèi)的數(shù)據(jù)包會(huì)被默默丟棄。通過(guò)構(gòu)造合法窗口范圍內(nèi)seq,可以造成RST攻擊,這一點(diǎn)大家了解就好,千萬(wàn)別學(xué)!
參考資料
TCP旁路攻擊分析與重現(xiàn) - https://www.cxyzjd.com/article/qq_27446553/52416369
最后
最近想用vscode寫(xiě)小說(shuō)了,故事梗概都想好了。
十年前,他是大廠(chǎng)最年輕CTO,閉眼刷leetcode,敲代碼 0 error ,0 warning, 卻被誣陷刪庫(kù)跑路,鋃鐺入獄,眾叛親離……十年后,他重新歸來(lái)!卻看到自己的女兒在仇人公司里修bug!
"我要你付出代價(jià)!"
一聲令下,十萬(wàn)
p7,p8應(yīng)聲前來(lái)…….
爽否?
如果文章對(duì)你有幫助,歡迎…..
算了。
兄弟們都是自家人,點(diǎn)不點(diǎn)贊,在不在看什么的,沒(méi)關(guān)系的,大家看開(kāi)心了就好。
在看,點(diǎn)贊什么的,我不是特別在意,真的,真的,別不信啊。
不三連也真的沒(méi)關(guān)系的。
兄弟們不要在意啊。
我是虛偽的小白,我們下期見(jiàn)!
別說(shuō)了,一起在知識(shí)的海洋里嗆水吧
點(diǎn)擊下方名片,關(guān)注公眾號(hào):【小白debug】
不滿(mǎn)足于在留言區(qū)說(shuō)騷話(huà)?
加我,我們建了個(gè)劃水吹牛皮群,在群里,你可以跟你下次跳槽可能遇到的同事或面試官聊點(diǎn)陽(yáng)間的話(huà)題。就超!開(kāi)!心!
