想必大家已經知道我的niao性,搞個標題,就是不喜歡立馬回答。
就是要搞一大堆原理性的東西,再回答標題的問題。
說這個是因為我這次會把問題的答案就放到開頭嗎?
不!
我就不!
但是大家可以直接根據目錄看自己感興趣的部分。
之所以要先鋪墊一些原理,還是希望大家能先看些基礎的,再慢慢循序漸進,這樣有利于建立知識體系。多一點上下文,少一點gap。
好了,進入正題。
下面是這篇文章的目錄。
收到RST就一定會斷開連接嗎
什么是RST
我們都知道TCP正常情況下斷開連接是用四次揮手,那是正常時候的優雅做法。
但異常情況下,收發雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機制去強行關閉連接。
RST 就是用于這種情況,一般用來異常地關閉一個連接。它是一個TCP包頭中的標志位。
正常情況下,不管是發出,還是收到置了這個標志位的數據包,相應的內存、端口等連接資源都會被釋放。從效果上來看就是TCP連接被關閉了。
而接收到 RST的一方,一般會看到一個 connection reset 或 connection refused 的報錯。
TCP報頭RST位
怎么知道收到RST了?
我們知道內核跟應用層是分開的兩層,網絡通信功能在內核,我們的客戶端或服務端屬于應用層。應用層只能通過 send/recv 與內核交互,才能感知到內核是不是收到了RST。
當本端收到遠端發來的RST后,內核已經認為此鏈接已經關閉。
此時如果本端應用層嘗試去執行 讀數據操作,比如recv,應用層就會收到 Connection reset by peer 的報錯,意思是遠端已經關閉連接。
ResetByPeer
如果本端應用層嘗試去執行寫數據操作,比如send,那么應用層就會收到 Broken pipe 的報錯,意思是發送通道已經壞了。
BrokenPipe
這兩個是開發過程中很經常遇到的報錯,感覺大家可以把這篇文章放進收藏夾吃灰了,等遇到這個問題了,再打開來擦擦灰,說不定對你會有幫助。
出現RST的場景有哪些
RST一般出現于異常情況,歸類為 對端的端口不可用 和 socket提前關閉。
端口不可用
端口不可用分為兩種情況。要么是這個端口從來就沒有"可用"過,比如根本就沒監聽(listen)過;要么就是曾經"可用",但現在"不可用"了,比如服務突然崩了。
端口未監聽
TCP連接未監聽的端口
服務端listen 方法會創建一個sock放入到全局的哈希表中。
此時客戶端發起一個connect請求到服務端。服務端在收到數據包之后,第一時間會根據IP和端口從哈希表里去獲取sock。
全局hash表
如果服務端執行過listen,就能從全局哈希表里拿到sock。
但如果服務端沒有執行過listen,那哈希表里也就不會有對應的sock,結果當然是拿不到。此時,正常情況下服務端會發RST給客戶端。
端口未監聽就一定會發RST嗎?
不一定。上面提到,發RST的前提是正常情況下,我們看下源碼。
// net/ipv4/tcp_ipv4.c
// 代碼經過刪減
int tcp_v4_rcv(struct sk_buff *skb)
{
// 根據ip、端口等信息 獲取sock。
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
no_tcp_socket:
// 檢查數據包有沒有出錯
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
// 錯誤記錄
} else {
// 發送RST
tcp_v4_send_reset(NULL, skb);
}
}
內核在收到數據后會從物理層、數據鏈路層、網絡層、傳輸層、應用層,一層一層往上傳遞。到傳輸層的時候,根據當前數據包的協議是TCP還是UDP走不一樣的函數方法。可以簡單認為,TCP數據包都會走到 tcp_v4_rcv()。這個方法會從全局哈希表里獲取 sock,如果此時服務端沒有listen()過 , 那肯定獲取不了sock,會跳轉到no_tcp_socket的邏輯。
注意這里會先走一個 tcp_checksum_complete(),目的是看看數據包的校驗和(Checksum)是否合法。
校驗和可以驗證數據從端到端的傳輸中是否出現異常。由發送端計算,然后由接收端驗證。計算范圍覆蓋數據包里的TCP首部和TCP數據。
如果在發送端到接收端傳輸過程中,數據發生任何改動,比如被第三方篡改,那么接收方能檢測到校驗和有差錯,此時TCP段會被直接丟棄。如果校驗和沒問題,那才會發RST。
所以,只有在數據包沒問題的情況下,比如校驗和沒問題,才會發RST包給對端。
為什么數據包異常的情況下,不發RST?
一個數據包連校驗都不能通過,那這個包,多半有問題。
有可能是在發送的過程中被篡改了,又或者,可能只是一個胡亂偽造的數據包。
五層網絡,不管是哪一層,只要遇到了這種數據,推薦的做法都是默默扔掉,而不是去回復一個消息告訴對方數據有問題。
如果對方用的是TCP,是可靠傳輸協議,發現很久沒有ACK響應,自己就會重傳。
如果對方用的是UDP,說明發送端已經接受了“不可靠會丟包”的事實,那丟了就丟了。
因此,數據包異常的情況下,默默扔掉,不發RST,非常合理。
還是不能理解?那我再舉個例子。
正常人噴你,他說話條理清晰,主謂賓分明。此時你噴回去,那你是個充滿熱情,正直,富有判斷力的好人。
而此時一個憨憨也想噴你,但他思維混亂,連話都說不清楚,一直阿巴阿巴的,你雖然聽不懂,但大受震撼,此時你會?
- A:跟他激情互噴
- B:不跟他一般見識,就當沒聽過
一般來說最優選擇是B,畢竟你理他,他反而來勁。
這下,應該就懂了。
程序啟動了但是崩了
端口不可用的場景里,除了端口未監聽以外,還有可能是從前監聽了,但服務端機器上做監聽操作的應用程序突然崩了,此時客戶端還像往常一樣正常發送消息,服務器內核協議棧收到消息后,則會回一個RST。在開發過程中,這種情況是最常見的。
比如你的服務端應用程序里,弄了個空指針,或者數組越界啥的,程序立馬就崩了。
TCP監聽了但崩了
這種情況跟端口未監聽本質上類似,在服務端的應用程序崩潰后,原來監聽的端口資源就被釋放了,從效果上來看,類似于處于CLOSED狀態。
此時服務端又收到了客戶端發來的消息,內核協議棧會根據IP端口,從全局哈希表里查找sock,結果當然是拿不到對應的sock數據,于是走了跟上面"端口未監聽"時一樣的邏輯,回了個RST。客戶端在收到RST后也釋放了sock資源,從效果上來看,就是連接斷了。
RST和502的關系
上面這張圖,服務端程序崩潰后,如果客戶端再有數據發送,會出現RST。但如果在客戶端和服務端中間再加一個Nginx,就像下圖一樣。
RST與502
nginx會作為客戶端和服務端之間的"中間人角色",負責轉發請求和響應結果。但當服務端程序崩潰,比如出現野指針或者OOM的問題,那轉發到服務器的請求,必然得不到響應,后端服務端還會返回一個RST給nginx。nginx在收到這個RST后會斷開與服務端的連接,同時返回客戶端一個502錯誤碼。
所以,出現502問題,一般情況下都是因為后端程序崩了,基于這一點假設,去看看監控是不是發生了OOM或者日志是否有空指針等報錯信息。
socket提前關閉
這種情況分為本端提前關閉,和遠端提前關閉。
本端提前關閉
如果本端socket接收緩沖區還有數據未讀,此時提前close() socket。那么本端會先把接收緩沖區的數據清空,然后給遠端發一個RST。
recvbuf非空
遠端提前關閉
遠端已經close()了socket,此時本端還嘗試發數據給遠端。那么遠端就會回一個RST。
close()觸發TCP四次揮手
大家知道,TCP是全雙工通信,意思是發送數據的同時,還可以接收數據。
Close()的含義是,此時要同時關閉發送和接收消息的功能。
客戶端執行close(), 正常情況下,會發出第一次揮手FIN,然后服務端回第二次揮手ACK。如果在第二次和第三次揮手之間,如果服務方還嘗試傳數據給客戶端,那么客戶端不僅不收這個消息,還會發一個RST消息到服務端。直接結束掉這次連接。
對方沒收到RST,會怎么樣?
我們知道TCP是可靠傳輸,意味著本端發一個數據,遠端在收到這個數據后就會回一個ACK,意思是"我收到這個包了"。
而RST,不需要ACK確認包。
因為RST本來就是設計來處理異常情況的,既然都已經在異常情況下了,還指望對方能正常回你一個ACK嗎?可以幻想,不要妄想。
但問題又來了,網絡環境這么復雜,丟包也是分分鐘的事情,既然RST包不需要ACK來確認,那萬一對方就是沒收到RST,會怎么樣?
RST丟失
RST丟了,問題不大。比方說上圖服務端,發了RST之后,服務端就認為連接不可用了。
如果客戶端之前發送了數據,一直沒等到這個數據的確認ACK,就會重發,重發的時候,自然就會觸發一個新的RST包。
而如果客戶端之前沒有發數據,但服務端的RST丟了,TCP有個keepalive機制,會定期發送探活包,這種數據包到了服務端,也會重新觸發一個RST。
RST丟失后keepalive
收到RST就一定會斷開連接嗎?
先說結論,不一定會斷開。我們看下源碼。
// net/ipv4/tcp_input.c
static bool tcp_validate_incoming()
{
// 獲取sock
struct tcp_sock *tp = tcp_sk(sk);
// step 1:先判斷seq是否合法(是否在合法接收窗口范圍內)
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
goto discard;
}
// step 2:執行收到 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包,第一步會通過tcp_sequence先看下這個seq是否合法,其實主要是看下這個seq是否在合法接收窗口范圍內。如果不在范圍內,這個RST包就會被丟棄。
至于接收窗口是個啥,我們先看下面這個圖。
接收窗口
這里黃色的部分,就是指接收窗口,只要RST包的seq不在這個窗口范圍內,那就會被丟棄。
為什么要校驗是否在窗口范圍內
正常情況下客戶端服務端雙方可以通過RST來斷開連接。假設不做seq校驗,如果這時候有不懷好意的第三方介入,構造了一個RST包,且在TCP和IP等報頭都填上客戶端的信息,發到服務端,那么服務端就會斷開這個連接。同理也可以偽造服務端的包發給客戶端。這就叫RST攻擊。
RST攻擊
受到RST攻擊時,從現象上看,客戶端老感覺服務端崩了,這非常影響用戶體驗。
如果這是個游戲,我相信多崩幾次,第二天大家就不來玩了。
實際消息發送過程中,接收窗口是不斷移動的,seq也是在飛快的變動中,此時第三方是比較難構造出合法seq的RST包的,那么通過這個seq校驗,就可以攔下了很多不合法的消息。
加了窗口校驗就不能用RST攻擊了嗎
不是,只是增加了攻擊的成本。但如果想搞,還是可搞的。
以下是面向監獄編程的環節。
希望大家只了解原理就好了,不建議使用。
相信大家都不喜歡穿著藍白條紋的衣服,拍純獄風的照片。
從上面可以知道,不是每一個RST包都會導致連接重置的,要求是這個RST包的seq要在窗口范圍內,所以,問題就變成了,我們怎么樣才能構造出合法的seq。
盲猜seq
窗口數值seq本質上只是個uint32類型。
struct tcp_skb_cb {
__u32 seq; /* Starting sequence number */
}
如果在這個范圍內瘋狂猜測seq數值,并構造對應的包,發到目的機器,雖然概率低,但是總是能被試出來,從而實現RST攻擊。這種亂棍打死老師傅的方式,就是所謂的合法窗口盲打(blind in-window attacks)。
覺得這種方式比較笨?那有沒有聰明點的方式,還真有,但是在這之前需要先看下面的這個問題。
已連接狀態下收到第一次握手包會怎么樣?
我們需要了解一個問題,比如服務端在已連接(ESTABLISHED)狀態下,如果收到客戶端發來的第一次握手包(SYN),會怎么樣?
以前我以為服務單會認為客戶端憨憨了,直接RST連接。
但實際,并不是。
static bool tcp_validate_incoming()
{
struct tcp_sock *tp = tcp_sk(sk);
/* 判斷seq是否在合法窗口內 */
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
if (!th->rst) {
// 收到一個不在合法窗口內的SYN包
if (th->syn)
goto syn_challenge;
}
}
/*
* RFC 5691 4.2 : 發送 challenge ack
*/
if (th->syn) {
syn_challenge:
tcp_send_challenge_ack(sk);
}
}
當客戶端發出一個不在合法窗口內的SYN包的時候,服務端會發一個帶有正確的seq數據ACK包出來,這個ACK包叫 challenge ack。
challenge ack抓包
上圖是抓包的結果,用scapy隨便偽造一個seq=5的包發到服務端(端口9090),服務端回復一個帶有正確seq值的challenge ack包給客戶端(端口8888)。
利用challenge ack獲取seq
上面提到的這個challenge ack ,仿佛為盲猜seq的老哥們打開了一個新世界。
在獲得這個challenge ack后,攻擊程序就可以以ack值為基礎,在一定范圍內設置seq,這樣造成RST攻擊的幾率就大大增加了。
利用ChallengeACK的RST攻擊
總結
- RST其實是TCP包頭里的一個標志位,目的是為了在異常情況下關閉連接。
- 內核收到RST后,應用層只能通過調用讀/寫操作來感知,此時會對應獲得 Connection reset by peer 和Broken pipe 報錯。
- 發出RST后不需要得到對方的ACK確認包,因此RST丟失后對方不能立刻感知,但是通過下一次重傳數據或keepalive心跳包可以導致RST重傳。
- 收到RST包,不一定會斷開連接,seq不在合法窗口范圍內的數據包會被默默丟棄。通過構造合法窗口范圍內seq,可以造成RST攻擊,這一點大家了解就好,千萬別學!
來源:
https://mp.weixin.qq.com/s/Fr6o6gRiIUIspV9-jR9snw