關於TCP關閉想到的一些問題


一、問題的引入
在客戶端希望通過http協議到服務器來拉取數據時,這種交互大多就是一次性的交互,客戶端從httpsvr把數據拉取回來之后,服務器會主動關閉套接口。通常來說,如果是我們通過傳統的PC端來連接,這個問題不是很大,因為這些客戶端通常就是專門圍着這個httpsvr來轉的,就等着httpsvr的回包來下鍋處理接下來的流程。客戶端可以這么任性的等待服務器,進而阻塞整個邏輯。但是在這個“客戶端”升級為一個代理服務器,它本身管理並維護了很多和httpsvr之間的多條連接,此時代理服務器就無法像PC客戶端那樣即時的處理httpsvr的回包,它不再是為一條鏈路負責,而是需要對多條鏈路負責,此時httpsvr的客戶端就不能保證自己會及時的處理httpsvr的回包(此時的這httpsvr客戶端應該會使用異步epoll等待,或者是通過定時器定時查詢)。進而導致的問題是當代理回過頭來有時間來處理httpsvr的回包時,可能httpsvr端的數據已經關閉了。這時候問題就來了,當httpsvr把socket關閉之后,假設代理服務器沒有處理httpsvr的回包嗎,當代理服務器抽出時間來讀取socket中httpsvr的回包時,這個數據還在嗎?再強調一下,此時httpsvr的服務器和代理服務器的socket連接已經關閉。
二、從TCP連接的主動關閉方來看
對於BSD socket API來說,一個socket的shutdown接口可以僅關閉發送,也可以僅關閉接收,當然也可以兩者都關閉。再看TCP協議中的關閉協議,主動方只是發送一個FIN標志位報文,這個FIN包對應的是shutdown的哪個動作呢?直觀上說,應該是在關閉讀操作時發送FIN包,因為接收關閉就意味着接下來發送過來的包要吃閉門羹,而這進一步意味着會有數據丟失(即發送方發送的數據沒有被接收方接收到),這通常無法容忍,這也就是SIGPIPE的意義。SIGPIPE信號的默認處理動作就是關閉整個進程,感覺操作系統的意思就是把事情鬧大,既然數據丟失你都不關系,這么不負責任,那索性把把你直接弄死吧。
事實上只有當shutdown“發送”時,socket才會向對方發送FIN數據包,這一點可能要從網絡的“全雙工”模型來看待這個問題。這里的雙工就是說這個鏈路上同時存在兩個方向上的數據流,任何一方只能關閉自己這一側的數據源,而不能直接明確告訴對方我關閉了數據接收。雖然通過shutdown關閉了自己在該端口上的讀操作,但事實上這僅僅是一個本地的狀態,並不會通過網絡傳遞給地方,事實上TCP的協議中也沒有這樣的協議來完成這樣的事件同步。
當主動關閉本地連接的send功能之后,如果有本地進程繼續向這個socket中寫入數據,會收到SIGPIPE信號;如果關閉本地socket的read功能,本地從中讀取數據時會馬上返回,讀取有效數據長度為0。
三、從TCP連接被動方來看
前面說到了當對端發送關閉之后,本地會收到對方發送的FIN字段,這個字段說明對方已經保證自己不會再發送數據過來了。這對於本地來說是一個重要信息,它意味着本地的所有當前讀取操作和后續的讀取操作都不用再等待了,所以所有這樣的阻塞進程可以被喚醒,接下來的所有讀取動作不會有有效數據返回了。從API的角度來看,如果傳入要求讀入數據的長度非零而read的返回值為0,則表示說這個文件已經到了文件的結尾,對於socket來說就是對端已經關閉。這個行為從代碼中來看主要體現在兩個方面,一個是從socket讀取時,tcp_recvmsg中判斷如果對端已經關閉則直接返回;查詢socket狀態時可讀狀態始終置位,相關代碼在tcp_poll中。
還有前面還說到過,對方關閉read並沒有通知到本地,如果本地socket在對端shutdown read之后還繼續向對方發送數據,就會收到對方的reset回包,這個包會導致之后從該socket 發送和接收數據都返回SIGPIPE錯誤,相關代碼在tcp_sendmsg和tcp_recvmsg中。
四、一方close socket之后另一方不關閉會怎樣
現在回到開始的問題,代理服務器發送請求,發送后開始休眠,等待下一個定時器到來時檢測回包,httpsvr在本地定時器等待時已經關閉了socket,本地操作系統進而完成了協議內的收發包邏輯,當代理服務器在定時器到期后檢測該socket時,服務器回包的數據還可以得到嗎?從TCP協議上看,主動關閉方並不能要求被動方什么時候關閉鏈路,它甚至不能要求對方是否一定關閉鏈路。當服務器關閉了socket,如果對端一直不關閉怎么辦呢?就這么一直傻等着那服務器很快就會被DOS攻擊了。
所以操作系統在socket關閉之后會設置一個定時器,用來處理協議的一些善后處理問題,這些狀態在TCP協議中有說明,在內核tcp.c文件開始的注釋中也有說明,代碼中流程為tcp_close-->>tcp_close_state中將socket進入TCP_FIN_WAIT1,在本地socket中所有buffer內數據發送成功並被對方ack之后進入在tcp_rcv_state_process中進入TCP_FIN_WAIT2,同時通過tcp_fin_time(sk)獲得超時時間,如果對方在這么長時間內一直不發送FIN包,那么對不起了,我先撤了,自己單方面關閉這個socket了。
從這個邏輯可以進一步看到,對端關閉事實上對本地socket已經接收到的數據是沒有任何影響的,也就是說,在對方socket關閉之前本地socket接收的數據在任何時候通過read接口來讀取依然可以獲得,前提是不要向對方寫入數據。因為寫入數據會收到對方的reset信息,操作系統在tcp_rcv_state_process-->>tcp_reset函數中設置sk->sk_err = EPIPE狀態,在讀取時tcp_recvmsg函數內if (sk->sk_err) copied = sock_error(sk);,這個發送動作可能就是所謂的豬一樣的隊友吧,如果不發送任何數據,對方不會回復reset,本地通過tcp_recvmsg還可以接收到對方的回包數據。
這里說的是通常服務器端回包數據較小,可以存儲在socket的recv緩沖區中,本地會對對端發送的所有數據進行ack,這樣對端可以順利的在超時之后完成老化關閉。
五、是否可能實現DOS攻擊
再繼續引申一個有趣的問題:假設說客戶端發送請求之后,故意不去讀取服務器的回包數據,那么此時是否能夠用多台機器對服務器形成DOS攻擊?回過頭來再看下tcp_close,它並不是一個阻塞的過程,當一個進程執行close動作時,此時這個socket(的緩沖區)中可以有未發送或者未確認的數據,這也就是FIN_WAIT1狀態的意義,它在等待所有的發送被確認之后才進入FIN_WAIT2。由於TCP中的發送和確認是操作系統層完成的,所以如果說服務器發送數據小於接收側socket的緩沖區,那么操作系統就會完成除了發送FIN之外的所有和服務器的TCP協議交互,服務器就可以在FIN_TIMEOUT之后釋放這個socket資源,客戶端如果不讀取的話那資源消耗在了客戶端,所以不能對服務器構成攻擊。
那么如果客戶端的緩沖區小於服務器回包的長度呢?此時服務器將會由於客戶端接收窗口不足出現阻塞等待,如果大量的客戶端這樣來阻塞服務器,那么此時服務器就可能會出現拒絕服務,在內核的tcp_close函數中有這么一層保護,保證系統中不會出現太多的orphan socket,並且它們不會消耗太多的資源,這就杜絕了通過DOS攻擊消耗掉操作系統資源的可能性
if (sk->sk_state != TCP_CLOSE) {
sk_stream_mem_reclaim(sk);
if (atomic_read(sk->sk_prot->orphan_count) > sysctl_tcp_max_orphans ||
    (sk->sk_wmem_queued > SOCK_MIN_SNDBUF &&
     atomic_read(&tcp_memory_allocated) > sysctl_tcp_mem[2])) {
if (net_ratelimit())
printk(KERN_INFO "TCP: too many of orphaned "
       "sockets\n");
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, GFP_ATOMIC);
NET_INC_STATS_BH(LINUX_MIB_TCPABORTONMEMORY);
}
}
這篇文章描述了這樣的現象和效果,作者強調到說要讓回包數據量大於socket的發送緩沖區,否則應用進程把數據發送給操作系統之后就相當於完成了這條鏈路的功能,進而回收了這個用戶態的socket pool,所以攻擊相當於攻擊了操作系統的資源而不是進程本身。
這里還有另一個TCP中經常提到的問題,就是zero windows detection,當一方知道對方接收窗口已經滿了之后,就會周期性的向對方發送 zero windows detection包,它和超時定時器的不同在於,它沒有超時錯誤(至少我沒看到),也就是可以嘗試任意多次並一直繼續下去,而超時定時器在一定次數之后會認為鏈路已經關閉。
這篇文章提到了這個問題,下面有人的回答是說用戶態進程可以輕易的知道是誰卡了這么久時間,進而結束掉進程。
六、如何知道對方已經關閉寫操作
對方發送FIN包之后,本地處理邏輯為:
static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
{
struct tcp_sock *tp = tcp_sk(sk);
 
inet_csk_schedule_ack(sk);
 
sk->sk_shutdown |= RCV_SHUTDOWN;
……
}
本地讀取數據時
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
……
if ( sk->sk_shutdown & RCV_SHUTDOWN)
break;
……
本地poll該socket狀態時:
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
……
if (sk->sk_shutdown & RCV_SHUTDOWN)
mask |= POLLIN | POLLRDNORM | POLLRDHUP;
}
也就是在異步epoll模式下,poll得到POLLIN狀態並且讀取socket中長度為零,可以認為對方已經關閉, 下面文章描述了通過read返回值判斷對方時候關閉了寫操作的可行性,並且該網頁也包含了很多常見的TCP相關的問題。
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM