深入理解 Linux 的 TCP 三次握手
作者 | zorrozou
前言
TCP協(xié)議是一個(gè)大家好像都熟悉,又好像都不熟悉的協(xié)議。說熟悉,是因?yàn)槲覀兓久刻於家玫剿腥怂坪鯇?duì)三次握手、四次揮手、滑動(dòng)窗口、慢啟動(dòng)、擁塞避免、擁塞控制等概念好像都有些了解。說不熟悉,是因?yàn)門CP協(xié)議相當(dāng)?shù)膹?fù)雜,而且在運(yùn)行過程中網(wǎng)絡(luò)環(huán)境會(huì)變化,TCP的相關(guān)機(jī)制也會(huì)因?yàn)椴?同的變化而產(chǎn)生相關(guān)的適應(yīng)行為,真的要說清楚其相關(guān)概念和運(yùn)行過程又真的很不容易。
本系列文章希望從另一個(gè)角度交代清楚Linux上TCP實(shí)現(xiàn)的部分細(xì)節(jié),當(dāng)然能力有限,有些交代不清楚的地方還希望大家海涵。本文就從TCP建立連接的三次握手開始,希望對(duì)你有所幫助。本文內(nèi)核代碼版本基于linux-5.3。
什么是可靠和面向連接?
說到TCP,不可不說的就是其是一個(gè)面向連接和可靠的傳輸層協(xié)議。相對(duì)的就是UDP,不可靠且非面向連接。其實(shí)IP的交付就是面向無連接和不可靠的協(xié)議,而UDP只是簡(jiǎn)單的在IP層協(xié)議上加了個(gè)傳輸層的端口封裝,所以自然繼承了IP的交付質(zhì)量。TCP之所以復(fù)雜,就是因?yàn)樗脑O(shè)計(jì)需要在一個(gè)面向無連接、不可靠的IP上實(shí)現(xiàn)一個(gè)面向連接、可靠的傳輸層協(xié)議。所以,我們需要先從工程角度理解清楚到底什么是面向連接?什么是可靠?才能理解TCP為啥要這么復(fù)雜。
我們先來概述一下這幾個(gè)問題:
什么是面向連接:
連接:在一個(gè)連接中傳輸?shù)臄?shù)據(jù)是有關(guān)系狀態(tài)的,比如需要確定傳輸?shù)膶?duì)端正處在等待發(fā)送或接收的狀態(tài)上。需要維護(hù)傳輸數(shù)據(jù)的關(guān)系,比如數(shù)據(jù)流的順序。典型的例子就是打電話。
無連接:不用關(guān)心對(duì)端是否在線。每一個(gè)數(shù)據(jù)段的發(fā)送都是獨(dú)立的一個(gè)數(shù)據(jù)個(gè)體,數(shù)據(jù)和數(shù)據(jù)之間沒有關(guān)系,無需維護(hù)其之間的關(guān)系。典型的例子就是發(fā)短信。
什么是可靠:
主要是指數(shù)據(jù)在傳輸過程中不會(huì)被損壞或者丟失,保證數(shù)據(jù)可以正確到達(dá)。而不做以上保證的就是不可靠。
如何解決面向連接問題:
使用建立連接,傳輸數(shù)據(jù),斷開連接的三步創(chuàng)建一個(gè)長(zhǎng)期的數(shù)據(jù)傳輸機(jī)制,在同一個(gè)連接中的數(shù)據(jù)傳輸是有上下文關(guān)系的。所以就要引申出以下概念:
- 需要維護(hù)seq序列號(hào)字段維護(hù)數(shù)據(jù)的順序關(guān)系保證按序交付,和解決數(shù)據(jù)包重復(fù)的問題。
- 需要部分特殊的狀態(tài)標(biāo)記的包來專門創(chuàng)建、斷開和維護(hù)一個(gè)連接:syn,ack,fin,rst
如何解決可靠性問題:
引入數(shù)據(jù)傳輸?shù)拇_認(rèn)機(jī)制,即數(shù)據(jù)發(fā)送之后等待對(duì)方確認(rèn)。于是需要維護(hù)確認(rèn)字段Acknowledgement和ack狀態(tài)。即:停止等待協(xié)議。
引入數(shù)據(jù)確認(rèn)機(jī)制(停止等待協(xié)議)之后,引發(fā)了帶寬利用律不高的問題,如何解決?解決方案是引入窗口確認(rèn)機(jī)制和滑動(dòng)窗口,即不在以每個(gè)包發(fā)送之后進(jìn)行確認(rèn),而是發(fā)送多個(gè)包之后一起確認(rèn)。
引入窗口之后,如何在不同延時(shí)的網(wǎng)絡(luò)上選擇不同窗口大???解決方法是引入窗口變量,和窗口監(jiān)測(cè)通告:
發(fā)送方維護(hù):
- 已發(fā)送并確認(rèn)ack偏移量(窗口左邊界)
- 已發(fā)送未確認(rèn)ack偏移量(窗口當(dāng)前發(fā)送字節(jié)位置)
- 即將發(fā)送偏移量(窗口右邊界)
接收方維護(hù):
- 已接受并確認(rèn)偏移量(窗口左邊界)
- 接受后會(huì)保存的窗口大?。ù翱谟疫吔纾?/li>
接收方會(huì)給發(fā)送方回復(fù)ack確認(rèn),ack中會(huì)有最新窗口通告長(zhǎng)度,以便發(fā)送方調(diào)整窗口長(zhǎng)度。此處會(huì)引入sack選擇確認(rèn)行為和窗口為0時(shí)的堅(jiān)持定時(shí)器行為。
引入滑動(dòng)窗口之后,帶寬可以充分被利用了,但是網(wǎng)絡(luò)環(huán)境是復(fù)雜的,隨時(shí)可能因?yàn)榇罅康臄?shù)據(jù)傳輸導(dǎo)致網(wǎng)絡(luò)上的擁塞。于是要引入擁塞控制機(jī)制:當(dāng)出現(xiàn)擁塞的時(shí)候,tcp應(yīng)該能保證帶寬是被每條tcp連接公平分享的。所以在擁塞的情況下,要能將占用帶寬較大的連接調(diào)整為占用帶寬變小,占用小的調(diào)大。以達(dá)到公平占用資源的目的。
擁塞控制對(duì)帶寬占用的調(diào)整本質(zhì)上就是調(diào)整滑動(dòng)窗口的大小來實(shí)現(xiàn)的,所以需要在接受端引入一個(gè)新的變量叫做cwnd:擁塞窗口,來反應(yīng)當(dāng)前網(wǎng)絡(luò)的傳輸能力,而之前的通告窗口可以表示為awnd。此時(shí)發(fā)送端實(shí)際可用的窗口為cwnd和awnd的較小者。
由此引發(fā)的各種問題和概念不一而足,比如:如何決定實(shí)際的通告窗口大???慢啟動(dòng)是什么?擁塞避免過程如何工作?擁塞控制是怎么作用的?等等等等......
TCP之所以復(fù)雜,根本原因就是要在工程上解決這些問題。思路概述完了,我們先來看三次握手到底是干嘛的。
為什么要三次?
為什么要三次握手,而不是兩次,或者四次?或者其他次數(shù)?
首先我們要先理解建立連接的目的,有兩個(gè):
- 確認(rèn)對(duì)端在線,即我請(qǐng)求你的時(shí)候你能立即給出響應(yīng)。(面向連接)
- 如果傳輸?shù)臄?shù)據(jù)多的話,要保證包的順序,所以要確認(rèn)這個(gè)鏈接中傳輸數(shù)據(jù)的起始序列號(hào)。因?yàn)閿?shù)據(jù)是雙向傳輸?shù)?,所以兩邊都要確認(rèn)對(duì)端的序列號(hào)。
確認(rèn)了第二個(gè)目的之后,我們就能理解,兩次握手至少讓一段無法確定對(duì)端是否了解了你的起始序列號(hào)。即,假設(shè)我是服務(wù)端。對(duì)端syn給我發(fā)了序列號(hào),我也給對(duì)端回了我的序列號(hào),但是如果我給對(duì)方發(fā)的這個(gè)數(shù)據(jù)包丟了怎么辦?于是我沒法確認(rèn)對(duì)端是否收到,所以需要對(duì)端再跟我確認(rèn)一下他確實(shí)收到了 。
那么非要四次的話也不是不行,只是太啰嗦了,所以三次是最合理的。不能免俗,我們還是用這個(gè)經(jīng)典的圖來看一下三次握手的過程。
我面試別人的時(shí)候經(jīng)常會(huì)在這里問一個(gè)比較弱智的問題:如果服務(wù)端A,在收到客戶端B發(fā)來的syn之后,并且回復(fù)了syn+ack之后,收到了從另一個(gè)客戶端C發(fā)來的ack包,請(qǐng)問此時(shí)服務(wù)端A會(huì)跟C建立后續(xù)的ESTABLISHED連接嗎?
畫成圖的話是這樣:
這個(gè)問題之所以說是比較弱智,是因?yàn)榇蠖鄶?shù)人都覺得不會(huì),但是如果再追問為什么的話,又很少人能真正答出來。那么為什么呢?其實(shí)也很簡(jiǎn)單,一個(gè)新的客戶端的ip+port都不一樣,直接給我發(fā)一個(gè)ack的話,根據(jù)tcp協(xié)議會(huì)直接回復(fù)rst,自然不會(huì)創(chuàng)建連接。這里其實(shí)引申出一個(gè)問題,內(nèi)核在這里要能識(shí)別出給我發(fā)這個(gè)ack請(qǐng)求的到底是第一次給我發(fā)的,還是之前有發(fā)過syn并且我已經(jīng)回復(fù)了syn+ack的。內(nèi)核會(huì)通過四元組進(jìn)行查詢,這個(gè)查詢會(huì)在tcp_v4_rcv()中執(zhí)行,就是tcp處理的總?cè)丝?,其中調(diào)用__inet_lookup()進(jìn)行查找。
static inline struct sock *__inet_lookup(struct net *net,
struct inet_hashinfo *hashinfo,
struct sk_buff *skb, int doff,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const __be16 dport,
const int dif, const int sdif,
bool *refcounted)
{
u16 hnum = ntohs(dport);
struct sock *sk;
sk = __inet_lookup_established(net, hashinfo, saddr, sport,
daddr, hnum, dif, sdif);
*refcounted = true;
if (sk)
return sk;
*refcounted = false;
return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
sport, daddr, hnum, dif, sdif);
}
查找分兩步,先檢查established中是否有連接,再檢查linstener中是否有連接,如果沒有就直接send_reset。確認(rèn)連接存在后,如果是TCP_ESTABLISHED狀態(tài),直接tcp_rcv_established()接收數(shù)據(jù),否則進(jìn)入tcp_rcv_state_process()處理tcp的各種狀態(tài)。如果是第一次握手,就是TCP_LISTEN狀態(tài),進(jìn)入:
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
此時(shí)conn_request為tcp_v4_conn_request(),在這個(gè)方法中進(jìn)行第一次握手的處理。如果是第三次握手,此時(shí)tcp狀態(tài)應(yīng)為:TCP_SYN_RECV。
服務(wù)端在SYN RECVED的狀態(tài)下,要在緩存中紀(jì)錄客戶端syn包中的內(nèi)容,以便在收包的過程中進(jìn)行查找,占用部分slab緩存。這個(gè)緩存在內(nèi)核中有個(gè)上限,用/proc/sys/net/ipv4/tcp_max_syn_backlog來限制緩存的個(gè)數(shù)。這個(gè)值決定了tcp再正常狀態(tài)下可以同時(shí)維持多少個(gè)TCP_SYN_RECV狀態(tài)的連接,即服務(wù)端半連接個(gè)數(shù)。一般服務(wù)器上的這個(gè)值默認(rèn)為1024-2048,這個(gè)值默認(rèn)情況會(huì)根據(jù)你的總內(nèi)存大小自動(dòng)產(chǎn)生,內(nèi)存大的值會(huì)大一些。
如果這個(gè)半連接隊(duì)列被耗盡了會(huì)怎么樣?我們依然可以在內(nèi)核中找到答案,在tcp_conn_request()中可以看到這樣一段:
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
這里相關(guān)幾個(gè)概念:
- syncookie是什么?
- inet_csk_reqsk_queue_len(sk)是什么?
我們后面會(huì)詳細(xì)說syncookie機(jī)制,這里先知道這樣一個(gè)結(jié)論即可:當(dāng)syncookie開啟的情況下,半連接隊(duì)列可認(rèn)為無上限。從inet_csk_reqsk_queue_len的定義可以知道其查看的是request_sock_queue結(jié)構(gòu)體中的qlen。結(jié)構(gòu)體定義如下:
/*
* For a TCP Fast Open listener -
* lock - protects the access to all the reqsk, which is co-owned by
* the listener and the child socket.
* qlen - pending TFO requests (still in TCP_SYN_RECV).
* max_qlen - max TFO reqs allowed before TFO is disabled.
*
* XXX (TFO) - ideally these fields can be made as part of "listen_sock"
* structure above. But there is some implementation difficulty due to
* listen_sock being part of request_sock_queue hence will be freed when
* a listener is stopped. But TFO related fields may continue to be
* accessed even after a listener is closed, until its sk_refcnt drops
* to 0 implying no more outstanding TFO reqs. One solution is to keep
* listen_opt around until sk_refcnt drops to 0. But there is some other
* complexity that needs to be resolved. E.g., a listener can be disabled
* temporarily through shutdown()->tcp_disconnect(), and re-enabled later.
*/
struct fastopen_queue {
struct request_sock *rskq_rst_head; /* Keep track of past TFO */
struct request_sock *rskq_rst_tail; /* requests that caused RST.
* This is part of the defense
* against spoofing attack.
*/
spinlock_t lock;
int qlen; /* # of pending (TCP_SYN_RECV) reqs */
int max_qlen; /* != 0 iff TFO is currently enabled */
struct tcp_fastopen_context __rcu *ctx; /* cipher context for cookie */
};
/** struct request_sock_queue - queue of request_socks
*
* @rskq_accept_head - FIFO head of established children
* @rskq_accept_tail - FIFO tail of established children
* @rskq_defer_accept - User waits for some data after accept()
*
*/
struct request_sock_queue {
spinlock_t rskq_lock;
u8 rskq_defer_accept;
u32 synflood_warned;
atomic_t qlen;
atomic_t young;
struct request_sock *rskq_accept_head;
struct request_sock *rskq_accept_tail;
struct fastopen_queue fastopenq; /* Check max_qlen != 0 to determine
* if TFO is enabled.
*/
};
這里又引申出一個(gè)新概念:TFO - TCP Fast Open,這里我們依舊先略過,放到后面講。這個(gè)結(jié)構(gòu)體中的qlen會(huì)在tcp_conn_request()函數(shù)執(zhí)行結(jié)束后增加:
if (fastopen_sk) {
af_ops->send_synack(fastopen_sk, dst, &fl, req,
&foc, TCP_SYNACK_FASTOPEN);
/* Add the child socket directly into the accept queue */
if (!inet_csk_reqsk_queue_add(sk, req, fastopen_sk)) {
reqsk_fastopen_remove(fastopen_sk, req, false);
bh_unlock_sock(fastopen_sk);
sock_put(fastopen_sk);
goto drop_and_free;
}
sk->sk_data_ready(sk);
bh_unlock_sock(fastopen_sk);
sock_put(fastopen_sk);
} else {
tcp_rsk(req)->tfo_listener = false;
if (!want_cookie)
inet_csk_reqsk_queue_hash_add(sk, req,
tcp_timeout_init((struct sock *)req));
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
if (want_cookie) {
reqsk_free(req);
return 0;
}
}
可以理解為,qlen為服務(wù)端listen端口的半連接隊(duì)列當(dāng)前長(zhǎng)度。于是這一段可以理解為:
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
當(dāng)沒開啟syncookie時(shí),如果當(dāng)前半連接池剩余長(zhǎng)度小于最大長(zhǎng)度的四分之一后,就不再處理新建連接請(qǐng)求了。這也就是著名的synflood攻擊的原理:
針對(duì)一個(gè)沒有syncookie功能的服務(wù)器,任意客戶端都可以通過構(gòu)造一個(gè)不完整三次握手過程,只發(fā)syn,不回第三次握手的ack來占滿服務(wù)端的半連接池,導(dǎo)致服務(wù)端無法再跟任何客戶端進(jìn)行tcp新建連接。
那么我們也就知道syncookie這個(gè)功能的設(shè)計(jì)初衷了:防止synflood。
syncookie如何防止synflood?
既然已經(jīng)明確synflood是針對(duì)半連接池上限的攻擊,那么我們就需要想辦法繞過去半連接池。能否讓服務(wù)器端不紀(jì)錄第一個(gè)syn發(fā)來的四元組信息,還能再第三次握手的時(shí)候做驗(yàn)證呢?其實(shí)也是可能的:既然三次握手的第二次是服務(wù)端回包,那為什么不把第一次握手得到的信息放到回包里,讓客戶端在第三 次握手的時(shí)候再把這個(gè)信息帶回來,然后我們拿到第三次握手的四元組信息和其中記錄的信息做驗(yàn)證不就好了?當(dāng)然,為了包內(nèi)容盡量小,我們把需要記錄到包里的信息做一下hash運(yùn)算,運(yùn)算出來的新數(shù)據(jù)就叫cookie。
具體處理方法描述如下:
在tcp_conn_request()中調(diào)用以下代碼產(chǎn)生cookie:
if (want_cookie) {
isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
req->cookie_ts = tmp_opt.tstamp_ok;
if (!tmp_opt.tstamp_ok)
inet_rsk(req)->ecn_ok = 0;
}
追溯到實(shí)際產(chǎn)生cookie的方法為:
static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,
__be16 dport, __u32 sseq, __u32 data)
{
/*
* Compute the secure sequence number.
* The output should be:
* HASH(sec1,saddr,sport,daddr,dport,sec1) + sseq + (count * 2^24)
* + (HASH(sec2,saddr,sport,daddr,dport,count,sec2) % 2^24).
* Where sseq is their sequence number and count increases every
* minute by 1.
* As an extra hack, we add a small "data" value that encodes the
* MSS into the second hash value.
*/
u32 count = tcp_cookie_time();
return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +
sseq + (count << COOKIEBITS) +
((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)
& COOKIEMASK));
}
根據(jù)包的四元組信息和當(dāng)前時(shí)間算出hash值,并記錄在isn中。發(fā)送synack使用tcp_v4_send_synack()函數(shù),其中調(diào)用tcp_make_synack(),判斷cookie_ts是不是被設(shè)置,如果被設(shè)置則初始化tcp選項(xiàng)信息到timestamp中的低6位。
#ifdef CONFIG_SYN_COOKIES
if (unlikely(req->cookie_ts))
skb->skb_mstamp_ns = cookie_init_timestamp(req);
else
#endif
這樣把synack發(fā)回給客戶端,包中包含了cookie信息??蛻舳嗽诨貜?fù)最后一個(gè)ack時(shí)將seq+1,就是說服務(wù)端收到最后一個(gè)ack的時(shí)候,只要將ack的seq序列號(hào)-1,就能拿到之前送出去的cookie。然后再根據(jù)包的四元組信息算一遍cookie,驗(yàn)證算出來的cookie和返回的cookie是不是一樣就行了。具體方法在cookie_v4_check()中,有興趣可以自行檢索代碼。
經(jīng)過這樣的驗(yàn)證,將原來需要內(nèi)存資源進(jìn)行處理的過程,完全轉(zhuǎn)變成了CPU運(yùn)算,這樣即使有synflood攻擊,攻擊的也不再是內(nèi)存上限,而是會(huì)轉(zhuǎn)換成CPU運(yùn)算,這樣會(huì)使攻擊的效果大大減弱。
syncookie功能內(nèi)核默認(rèn)是打開的,開關(guān)在:
/proc/sys/net/ipv4/tcp_syncookies
這個(gè)文件默認(rèn)值為1,代表打開syncookie功能。要注意的是,這種場(chǎng)景下,只有在tcp_max_syn_backlog上限被耗盡之后,新建的連接才會(huì)使用syncookie。設(shè)置0為關(guān)閉syncookie,設(shè)置為2為忽略tcp_max_syn_backlog半連接隊(duì)列,直接使用syncookie。
listen backlog
這里還要額外說明一下的是listen系統(tǒng)調(diào)用的backlog參數(shù)。我們都知道,要讓一個(gè)端口處在監(jiān)聽狀態(tài),需要調(diào)用socket、bind、listen三個(gè)系統(tǒng)調(diào)用,而最終TCP進(jìn)入LISTEN狀態(tài),就是由listen系統(tǒng)調(diào)用來做的。這個(gè)系統(tǒng)調(diào)用的第二個(gè)參數(shù)backlog在man page中解釋如下:
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.
從描述上看,這個(gè)backlog似乎限制了tcp的半連接隊(duì)列,但是如果你看man page可以細(xì)心一點(diǎn),再往下翻翻,就可以看到這段內(nèi)容:
NOTES
To accept connections, the following steps are performed:
1. A socket is created with socket(2).
2. The socket is bound to a local address using bind(2), so that other sockets may be connect(2)ed to it.
3. A willingness to accept incoming connections and a queue limit for incoming connections are specified with listen().
4. Connections are accepted with accept(2).
POSIX.1-2001 does not require the inclusion of <sys/types.h>, and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it.
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128.
這段內(nèi)容真正解釋了這個(gè)backlog的真實(shí)含義。簡(jiǎn)單來說就是,一個(gè)tcp連接的建立主要包括4部:
(1) 創(chuàng)建一個(gè)socket。socket()
(2) 將socket和本地某個(gè)地址和端口綁定。bind()
(3) 將socket值為listen狀態(tài)。listen()
此時(shí),客戶端就可以跟對(duì)應(yīng)端口的socket創(chuàng)建連接了,當(dāng)然這里如果創(chuàng)建連接的話,主要是可以完成三次握手。而還不能創(chuàng)建一個(gè)應(yīng)用可讀寫的連接。最終要?jiǎng)?chuàng)建一個(gè)真正可用的連接還需要第四部:
(4) 服務(wù)端accept一個(gè)新建連接,并建立一個(gè)新的accept返回的fd。accept()
這之后,服務(wù)端才可以用這個(gè)新的accept fd跟客戶端進(jìn)行通信。而這里的listen backlog,限制的就是,如果服務(wù)端處在LISTEN狀態(tài),并且有客戶端跟我建立連接,但是如果服務(wù)端沒有及時(shí)accept新建連接的話,那么這種還未accpet的請(qǐng)求隊(duì)列是多大?這里還有個(gè)問題,當(dāng)這個(gè)連接隊(duì)列超限之后,是什么 效果?
我們可以寫一個(gè)簡(jiǎn)單的服務(wù)端程序來測(cè)試一下這個(gè)狀態(tài),server端代碼如下:
[root@localhost basic]# cat server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
int sfd, afd;
socklen_t socklen;
struct sockaddr_in saddr, caddr;
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0) {
perror("socket()");
exit(1);
}
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
if (inet_pton(AF_INET, "0.0.0.0", &saddr.sin_addr) <= 0) {
perror("inet_pton()");
exit(1);
}
//saddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
perror("bind()");
exit(1);
}
if (listen(sfd, 5) < 0) {
perror("listen()");
exit(1);
}
pause();
while (1) {
bzero(&caddr, sizeof(caddr));
afd = accept(sfd, (struct sockaddr *)&caddr, &socklen);
if (afd < 0) {
perror("accept()");
exit(1);
}
if (write(afd, "hello", strlen("hello")) < 0) {
perror("write()");
exit(1);
}
close(afd);
}
exit(0);
}
代碼很簡(jiǎn)單,socket、bind、listen之后直接pause,我們來看看當(dāng)前狀態(tài):
[root@localhost basic]# ./server &
[1] 14141
[root@localhost basic]# ss -tnal
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 0.0.0.0:8888 0.0.0.0:*
此時(shí),對(duì)于ss命令顯示的LISTEN狀態(tài)連接,Send-Q數(shù)字的含義就是listen backlog的長(zhǎng)度。我們使用telnet作為客戶端連接當(dāng)前8888端口,并抓包看TCP的連接過程和其變化。
[root@localhost basic]# tcpdump -i ens33 -nn port 8888
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
10:54:41.863704 IP 192.168.247.130.45790 > 192.168.247.129.8888: Flags [S], seq 3982567931, win 64240, options [mss 1460,sackOK,TS val 1977602046 ecr 0,nop,wscale 7], length 0
10:54:41.863788 IP 192.168.247.129.8888 > 192.168.247.130.45790: Flags [S.], seq 3708893655, ack 3982567932, win 28960, options [mss 1460,sackOK,TS val 763077058 ecr 1977602046,nop,wscale 7], length 0
10:54:41.864005 IP 192.168.247.130.45790 > 192.168.247.129.8888: Flags [.], ack 1, win 502, options [nop,nop,TS val 1977602046 ecr 763077058], length 0
三次握手建立沒問題。
[root@localhost zorro]# ss -antl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 1 5 0.0.0.0:8888 0.0.0.0:*
ss顯示可知,LISTEN狀態(tài)的Recv-Q就是當(dāng)前在backlog隊(duì)列里排隊(duì)的連接個(gè)數(shù),我們多創(chuàng)建幾個(gè)看超限會(huì)如何:
[root@localhost zorro]# ss -antl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 6 5 0.0.0.0:8888 0.0.0.0:*
[root@localhost basic]# tcpdump -i ens33 -nn port 8888
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
11:00:40.674176 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977960856 ecr 0,nop,wscale 7], length 0
11:00:41.682431 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977961864 ecr 0,nop,wscale 7], length 0
11:00:43.728894 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977963911 ecr 0,nop,wscale 7], length 0
11:00:47.761967 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977967944 ecr 0,nop,wscale 7], length 0
11:00:56.017547 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977976199 ecr 0,nop,wscale 7], length 0
11:01:12.402559 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1977992584 ecr 0,nop,wscale 7], length 0
11:01:44.657797 IP 192.168.247.130.45804 > 192.168.247.129.8888: Flags [S], seq 3183080621, win 64240, options [mss 1460,sackOK,TS val 1978024840 ecr 0,nop,wscale 7], length 0
當(dāng)連接個(gè)數(shù)超過6再創(chuàng)建新的連接后,新連接已經(jīng)無法完成三次握手了,client的syn沒有收到回應(yīng),開始重試,重試6次結(jié)束連接。客戶端報(bào)錯(cuò)為:
[root@localhost zorro]# telnet 192.168.247.129 8888
Trying 192.168.247.129...
telnet: connect to address 192.168.247.129: Connection timed out
第一次握手的syn沒有收到回應(yīng)的情況下重試次數(shù)收到這個(gè)內(nèi)核參數(shù)限制:
/proc/sys/net/ipv4/tcp_syn_retries
設(shè)置第一次握手syn在沒有收到synack的情況下,最大重試次數(shù),默認(rèn)為6次??梢孕薷倪@個(gè)值達(dá)到改變重試次數(shù)的目的。但是時(shí)間規(guī)則無法改變。間隔時(shí)間是按照2的指數(shù)增長(zhǎng)的,就是說第一次重試是1秒,第二次為2秒,然后是4秒,8秒以此類推。所以默認(rèn)情況下tcp_syn_retries最多等待63秒。另外還有一個(gè)文件用來規(guī)定第二次握手的重試次數(shù):
/proc/sys/net/ipv4/tcp_synack_retries
設(shè)置第二次握手synack發(fā)出之后,在沒有收到最后一個(gè)ack的情況下,最大重試次數(shù),默認(rèn)值為5。所以tcp_synack_retries最多等待31秒。
根據(jù)以上測(cè)試我們發(fā)現(xiàn),當(dāng)listen backlog隊(duì)列被耗盡之后,新建連接是不能完成三次握手的,這有時(shí)候會(huì)跟synflood攻擊產(chǎn)生混淆,因?yàn)檫@跟synflood導(dǎo)致的效果類似。在內(nèi)核中的tcp_conn_request()處理過程中我們可以看到內(nèi)核應(yīng)對(duì)synflood和listen backlog滿的分別應(yīng)對(duì)方法:
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
struct tcp_fastopen_cookie foc = { .len = -1 };
__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
struct tcp_options_received tmp_opt;
struct tcp_sock *tp = tcp_sk(sk);
struct net *net = sock_net(sk);
struct sock *fastopen_sk = NULL;
struct request_sock *req;
bool want_cookie = false;
struct dst_entry *dst;
struct flowi fl;
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
......
當(dāng)sk_acceptq_is_full(sk)時(shí)直接drop,并且會(huì)增加LINUX_MIB_LISTENOVERFLOWS對(duì)應(yīng)的計(jì)數(shù)器。查詢計(jì)數(shù)器對(duì)應(yīng)關(guān)系我們可知,LINUX_MIB_LISTENOVERFLOWS對(duì)應(yīng)的是:
SNMP_MIB_ITEM("ListenOverflows", LINUX_MIB_LISTENOVERFLOWS)
也就是/proc/net/netstat中的ListenOverflows計(jì)數(shù)。這個(gè)計(jì)數(shù)也對(duì)應(yīng)netstat -s中顯示的listen queue of a socket overflowed。從sk_acceptq_is_full的代碼中我們可以看到,為什么listen backlog設(shè)置為5時(shí),必須連接數(shù)超過5+1才會(huì)連接超時(shí),因?yàn)楫?dāng)前連接個(gè)數(shù)必須大于最大上限:
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
從listen的man page中我們可以知道,listen backlog的內(nèi)核限制文件為:
/proc/sys/net/core/somaxconn
針對(duì)某個(gè)listen socket,當(dāng)前生效的配置是somaxconn和backlog的最小值。
在一般情況下,這個(gè)值是不用做優(yōu)化的。我們可以想象一下什么時(shí)候我們的應(yīng)用程序會(huì)在連接建立的時(shí)候來不及accept?大多數(shù)情況是當(dāng)你系統(tǒng)負(fù)載壓力特別大,以至于來不及處理新建連接的accept時(shí),這種情況下更重要的應(yīng)該去擴(kuò)容了,而非增加這個(gè)隊(duì)列。在這種情況下,有時(shí)甚至我們應(yīng)該調(diào)小這個(gè)隊(duì)列,并把客戶端的syn重試次數(shù)減少,以便能夠讓客戶端更快速的失敗,防止連接積累過多導(dǎo)致雪崩。當(dāng)然,部分并發(fā)處理架構(gòu)設(shè)計(jì)不好的軟件也會(huì)在非負(fù)載壓力大的時(shí)候耗盡這個(gè)隊(duì)列,這時(shí)候主要該調(diào)整的是軟件架構(gòu)或其他設(shè)置。
TFO - TCP Fast Open
從上面代碼中我們可以知道,當(dāng)前Linux TCP協(xié)議棧是支持TFO的。TFO,中文名字叫TCP快速打開。顧名思義,其主要目的就是簡(jiǎn)化三次握手的過程,讓TCP在大延時(shí)網(wǎng)絡(luò)上打開速度更快。那么TFO是如何工作的呢?我們可以先從一個(gè)實(shí)際的例子來觀察TFO的行為。
我們?cè)谝慌_(tái)服務(wù)器上開啟web服務(wù),另一臺(tái)服務(wù)器使用curl訪問web服務(wù)器的80端口之后訪問其/頁面后退出,抓到的包內(nèi)容如下:
[root@localhost zorro]# tcpdump -i ens33 -nn port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
11:31:07.390934 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [S], seq 4136264279, win 64240, options [mss 1460,sackOK,TS val 667677346 ecr 0,nop,wscale 7], length 0
11:31:07.390994 IP 192.168.247.129.80 > 192.168.247.130.58066: Flags [S.], seq 1980017862, ack 4136264280, win 28960, options [mss 1460,sackOK,TS val 4227985538 ecr 667677346,nop,wscale 7], length 0
11:31:07.391147 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 667677347 ecr 4227985538], length 0
11:31:07.391177 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 667677347 ecr 4227985538], length 79: HTTP: GET / HTTP/1.1
11:31:07.391185 IP 192.168.247.129.80 > 192.168.247.130.58066: Flags [.], ack 80, win 227, options [nop,nop,TS val 4227985538 ecr 667677347], length 0
11:31:07.391362 IP 192.168.247.129.80 > 192.168.247.130.58066: Flags [.], seq 1:2897, ack 80, win 227, options [nop,nop,TS val 4227985538 ecr 667677347], length 2896: HTTP: HTTP/1.1 200 OK
11:31:07.391441 IP 192.168.247.129.80 > 192.168.247.130.58066: Flags [P.], seq 2897:4297, ack 80, win 227, options [nop,nop,TS val 4227985539 ecr 667677347], length 1400: HTTP
11:31:07.391497 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [.], ack 2897, win 496, options [nop,nop,TS val 667677347 ecr 4227985538], length 0
11:31:07.391632 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [.], ack 4297, win 501, options [nop,nop,TS val 667677347 ecr 4227985539], length 0
11:31:07.398223 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [F.], seq 80, ack 4297, win 501, options [nop,nop,TS val 667677354 ecr 4227985539], length 0
11:31:07.398336 IP 192.168.247.129.80 > 192.168.247.130.58066: Flags [F.], seq 4297, ack 81, win 227, options [nop,nop,TS val 4227985545 ecr 667677354], length 0
11:31:07.398480 IP 192.168.247.130.58066 > 192.168.247.129.80: Flags [.], ack 4298, win 501, options [nop,nop,TS val 667677354 ecr 4227985545], length 0
這是一次完整的三次握手和四次揮手過程,還有一個(gè)http的數(shù)據(jù)傳輸過程。當(dāng)然,我們當(dāng)前并沒有開啟tfo,我們還是用一個(gè)圖來表達(dá)一下這個(gè)連接過程:
我們幸運(yùn)的在這個(gè)連接過程中觀察到了一次三次揮手關(guān)閉連接,但那不是我們今天的主題。其他連接過程基本就是標(biāo)準(zhǔn)的tcp行為。之后我們打開TFO看看是不是有什么變化。
我們的web服務(wù)器使用nginx,客戶端使用curl,這兩個(gè)軟件默認(rèn)情況下都是支持TFO的。首先內(nèi)核開啟TFO支持:
[root@localhost zorro]# echo 3 > /proc/sys/net/ipv4/tcp_fastopen
[root@localhost zorro]# cat /proc/sys/net/ipv4/tcp_fastopen
3
這個(gè)文件是TFO的開關(guān),0:關(guān)閉,1:打開客戶端支持,也是默認(rèn)值,2:打開服務(wù)端支持,3:打開客戶端和服務(wù)端。一般在需要的情況下,我們?cè)赾lient的服務(wù)器上設(shè)置為1,在server端的服務(wù)器上設(shè)置為2。方便起見,都設(shè)置為3也行。然后服務(wù)器nginx上配置打開TFO:
server {
listen 80 default_server fastopen=128;
listen [::]:80 default_server fastopen=128;
server_name _;
找到nginx的配置文件中l(wèi)isten的設(shè)置,加個(gè)fastopen參數(shù),后面跟一個(gè)值,如上所示。然后重啟服務(wù)器。
客戶端比較簡(jiǎn)單,使用curl的--tcp-fastopen參數(shù)即可,我們?cè)诳蛻舳藞?zhí)行這個(gè)命令:
[root@localhost zorro]# curl --tcp-fastopen http://192.168.247.129/
同時(shí)在服務(wù)端抓包看一下:
第一次請(qǐng)求抓包:
[root@localhost zorro]# tcpdump -i ens33 -nn port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
11:44:03.774234 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [S], seq 3253027385, win 64240, options [mss 1460,sackOK,TS val 668453730 ecr 0,nop,wscale 7,tfo cookiereq,nop,nop], length 0
11:44:03.774361 IP 192.168.247.129.80 > 192.168.247.130.58074: Flags [S.], seq 3812865995, ack 3253027386, win 28960, options [mss 1460,sackOK,TS val 4228761923 ecr 668453730,nop,wscale 7,tfo cookie 336fe8751f5cca4b,nop,nop], length 0
11:44:03.774540 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 668453730 ecr 4228761923], length 0
11:44:03.774575 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 668453730 ecr 4228761923], length 79: HTTP: GET / HTTP/1.1
11:44:03.774597 IP 192.168.247.129.80 > 192.168.247.130.58074: Flags [.], ack 80, win 227, options [nop,nop,TS val 4228761923 ecr 668453730], length 0
11:44:03.774786 IP 192.168.247.129.80 > 192.168.247.130.58074: Flags [.], seq 1:2897, ack 80, win 227, options [nop,nop,TS val 4228761923 ecr 668453730], length 2896: HTTP: HTTP/1.1 200 OK
11:44:03.774889 IP 192.168.247.129.80 > 192.168.247.130.58074: Flags [P.], seq 2897:4297, ack 80, win 227, options [nop,nop,TS val 4228761923 ecr 668453730], length 1400: HTTP
11:44:03.774997 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [.], ack 2897, win 496, options [nop,nop,TS val 668453731 ecr 4228761923], length 0
11:44:03.775022 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [.], ack 4297, win 489, options [nop,nop,TS val 668453731 ecr 4228761923], length 0
11:44:03.775352 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [F.], seq 80, ack 4297, win 501, options [nop,nop,TS val 668453731 ecr 4228761923], length 0
11:44:03.775455 IP 192.168.247.129.80 > 192.168.247.130.58074: Flags [F.], seq 4297, ack 81, win 227, options [nop,nop,TS val 4228761924 ecr 668453731], length 0
11:44:03.775679 IP 192.168.247.130.58074 > 192.168.247.129.80: Flags [.], ack 4298, win 501, options [nop,nop,TS val 668453731 ecr 4228761924], length 0
第二次請(qǐng)求抓包:
11:44:11.476255 IP 192.168.247.130.58076 > 192.168.247.129.80: Flags [S], seq 3310765845:3310765924, win 64240, options [mss 1460,sackOK,TS val 668461432 ecr 0,nop,wscale 7,tfo cookie 336fe8751f5cca4b,nop,nop], length 79: HTTP: GET / HTTP/1.1
11:44:11.476334 IP 192.168.247.129.80 > 192.168.247.130.58076: Flags [S.], seq 2616505126, ack 3310765925, win 28960, options [mss 1460,sackOK,TS val 4228769625 ecr 668461432,nop,wscale 7], length 0
11:44:11.476601 IP 192.168.247.129.80 > 192.168.247.130.58076: Flags [.], seq 1:2897, ack 1, win 227, options [nop,nop,TS val 4228769625 ecr 668461432], length 2896: HTTP: HTTP/1.1 200 OK
11:44:11.476619 IP 192.168.247.129.80 > 192.168.247.130.58076: Flags [P.], seq 2897:4297, ack 1, win 227, options [nop,nop,TS val 4228769625 ecr 668461432], length 1400: HTTP
11:44:11.476657 IP 192.168.247.130.58076 > 192.168.247.129.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 668461432 ecr 4228769625], length 0
11:44:11.476906 IP 192.168.247.130.58076 > 192.168.247.129.80: Flags [.], ack 4297, win 489, options [nop,nop,TS val 668461433 ecr 4228769625], length 0
11:44:11.477100 IP 192.168.247.130.58076 > 192.168.247.129.80: Flags [F.], seq 1, ack 4297, win 501, options [nop,nop,TS val 668461433 ecr 4228769625], length 0
11:44:11.477198 IP 192.168.247.129.80 > 192.168.247.130.58076: Flags [F.], seq 4297, ack 2, win 227, options [nop,nop,TS val 4228769625 ecr 668461433], length 0
11:44:11.477301 IP 192.168.247.130.58076 > 192.168.247.129.80: Flags [.], ack 4298, win 501, options [nop,nop,TS val 668461433 ecr 4228769625], length 0
我們發(fā)現(xiàn)開啟TFO之后,第一次http請(qǐng)求,tcp整體交互方式跟沒開啟之前基本一樣。第一次握手的syn多了cookiereq字段。第二次握手服務(wù)器端回復(fù)了cookie 336fe8751f5cca4b字段。這就是開啟了TFO之后,同一個(gè)客戶端請(qǐng)求的第一個(gè)tcp連接的主要交互差別:
客戶端的syn包中會(huì)帶一個(gè)空的cookie字段,服務(wù)器如果也支持TFO,那么看到這個(gè)空cookie字段后,會(huì)計(jì)算一個(gè)TFO cookie,然后回復(fù)給客戶端。這個(gè)cookie是用在下一次這個(gè)客戶端再跟服務(wù)端建立TCP連接的時(shí)候用的,帶cookie的syn包表示包內(nèi)還有承載應(yīng)用層數(shù)據(jù),這樣后續(xù)的TCP三次握手過程就可以不 僅僅是握手作用,還可以承載http協(xié)議數(shù)據(jù)了。圖示兩次交互如下:
我們?cè)賮砜匆幌赂鶷FO有關(guān)的其他內(nèi)核參數(shù):
/proc/sys/net/ipv4/tcp_fastopen
通過上述內(nèi)容我們已經(jīng)知道,這個(gè)文件值為1,2,3的含義。除了這些值以外,我們還可以設(shè)置為:
- 0x4 :對(duì)客戶端有效。不論Cookie是否可用且沒有cookie選項(xiàng),都將在SYN中發(fā)送數(shù)據(jù)。
- 0x200 :對(duì)服務(wù)器端有效。接受沒有任何Cookie選項(xiàng)的SYN數(shù)據(jù)。
- 0x400 :對(duì)服務(wù)器端有效。默認(rèn)情況下,使所有l(wèi)isten端口都支持TFO,而無需設(shè)置TCP_FASTOPEN套接字選項(xiàng)。
這里還需要補(bǔ)充的是,在一般情況下,除了內(nèi)核打開相關(guān)開關(guān)以外,應(yīng)用程序要支持TFO還要做相關(guān)調(diào)整。對(duì)于客戶端來說,需要使用sendmsg()或sendto()來法送數(shù)據(jù),并且要在flag參數(shù)中添加MSG_FASTOPEN標(biāo)記。對(duì)于服務(wù)端來說,需要在socket打開后,使用setsockopt設(shè)置TCP_FASTOPEN選項(xiàng)來打開TFO支 持。
/proc/sys/net/ipv4/tcp_fastopen_key
打開TFO后,內(nèi)核產(chǎn)生的cookie需要一個(gè)密鑰。默認(rèn)情況下,在打開了TFO的系統(tǒng)上,每個(gè)TCP連接所產(chǎn)生的密鑰都是內(nèi)核隨機(jī)產(chǎn)生的。除此之外,還可以使用setsockopt的TCP_FASTOPEN_KEY參數(shù)來設(shè)置密鑰,當(dāng)沒有設(shè)置的時(shí)候,系統(tǒng)會(huì)用這個(gè)文件中的值來作為密鑰。這個(gè)文件中密鑰指定為4個(gè)8位十六進(jìn)制整 數(shù),并用'-'分隔:xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx。前導(dǎo)零可以省略。可以通過用逗號(hào)分隔主密鑰和備份密鑰指定它們。主密鑰用于創(chuàng)建和驗(yàn)證cookie,而可選的備用密鑰僅用于驗(yàn)證cookie。如果僅指定一個(gè)密鑰,它將成為主密鑰,并且任何先前配置的備份密鑰都將被刪除。默認(rèn)值為:00000000-00000000-00000000-00000000,表示內(nèi)核隨機(jī)產(chǎn)生密鑰。
/proc/sys/net/ipv4/tcp_fastopen_blackhole_timeout_sec
因?yàn)門FO修改了正常tcp三次握手的過程,在第一個(gè)syn包經(jīng)過網(wǎng)絡(luò)到達(dá)服務(wù)器期間,有可能部分路由器或防火墻規(guī)則會(huì)把這種特殊的syn當(dāng)成異常流量而禁止掉。在這個(gè)語境下,我們把這種現(xiàn)象叫做TFO firewall blackhole。默認(rèn)的機(jī)制是,如果檢測(cè)到防火墻黑洞,則觸發(fā)暫時(shí)關(guān)閉TFO功能,這個(gè)文件就是用 來設(shè)置關(guān)閉時(shí)間周期的,默認(rèn)值為:3600,表示如果檢測(cè)到黑洞,則在3600秒內(nèi)關(guān)閉TFO。并且在第每個(gè)關(guān)閉周期結(jié)束后,如果再次檢測(cè)還發(fā)現(xiàn)有黑洞,則下次關(guān)閉周期時(shí)間是將會(huì)成倍增長(zhǎng)。值為0表示關(guān)閉黑洞檢測(cè)。
另外就是nginx配置中的fastopen=128的128是啥意思:其實(shí)就是限制打開fastopen后,針對(duì)這個(gè)端口未完成三次握手連接的最大長(zhǎng)度限制。這個(gè)限制可以開的大些。具體可以參見nginx的配置說明:nginx.org/en/docs/ht... 。
TFO的內(nèi)核代碼實(shí)現(xiàn),這里不再詳述了。大家可以在上面描述過的各個(gè)代碼中找到TFO相關(guān)的處理過程,有基礎(chǔ)的可以自行研究。關(guān)于服務(wù)器是否打開TFO仍然是一件爭(zhēng)論不休的事情,在復(fù)雜的網(wǎng)絡(luò)環(huán)境中,TFO的表現(xiàn)似乎距離大家的理想還差得有點(diǎn)遠(yuǎn),當(dāng)然并不是TFO不好,而是網(wǎng)絡(luò)經(jīng)常會(huì)把相關(guān)包給拒絕掉 ,導(dǎo)致TFO實(shí)際沒有生效。
有關(guān)TFO的更詳細(xì)說明,大家還可以參考RFC7413:tools.ietf.org/html/...
其他內(nèi)核參數(shù)
三次握手的過程中,還有幾個(gè)重試設(shè)置:
/proc/sys/net/ipv4/tcp_synack_retries
設(shè)置第二次握手synack發(fā)出之后,在沒有收到最后一個(gè)ack的情況下,最大重試次數(shù),默認(rèn)值為5。
這里只有重試次數(shù)設(shè)置,并沒有重試間隔時(shí)間。間隔時(shí)間是按照2的指數(shù)增長(zhǎng)的,就是說第一次重試是1秒,第二次為2秒,然后是4秒,8秒以此類推。所以默認(rèn)情況下tcp_syn_retries最多等待63秒,tcp_synack_retries最多等待31秒。