[轉帖]一個NAT問題引起的思考

济南小老虎發表於2024-06-20
https://perthcharles.github.io/2015/08/27/timestamp-NAT/


問題

當伺服器同時開啟tcp_timestamps和tcp_tw_recycle選項時,會導致客戶反饋連線成功率降低的情況。
but why ???


公網NAT的存在

NAT的全稱是:Network Address Translation
一個具體的例子就是家用的區域網路。
當使用一臺無線路由器進行上網撥號後,其他的終端裝置只要連線進入該無線路由器的WiFi
網路內就可以訪問外網了。此時正是NAT在發揮作用。
每一臺終端裝置在接入無線路由器後,只是獲得一個區域網IP地址,而當你在百度輸入我的IP的時候
你看到的IP地址則是你無線路由器的公網IP地址。家用無線路由器完成的一個主要工作正是將終端
的區域網IP地址進行NAT轉換為公網IP地址。

從上面這個簡單的例子可以看到NAT在真實的網際網路中是普遍存在的,比如你所在學校,單位都會一定程度上的使用NAT機制。


Per-host PAWS機制

這篇介紹TCP timestamp
的文章中提到了一種針對per-host的PAWS機制。這種機制要求所有來個同一個host IP的TCP資料包的
timestamp值是遞增的。當收到一個timestamp值,小於服務端記錄的對應值後,則會認為這是一個過期的資料包,然後會將其丟棄。


解答問題

至此就不難解釋為什麼在同時開啟tcp_timestamp和tcp_tw_recycle時,會遇到客戶反饋連線成功率降低的情況了,基本的邏輯如下:

1. 同時開啟tcp_timestamp和tcp_tw_recycle會啟用TCP/IP協議棧的per-host的PAWS機制
2. 經過同一NAT轉換後的來自不同真實client的資料流,在服務端看來是於同一host打交道
3. 雖然經過同一NAT轉化,但由於不同真實client會攜帶各自的timestamp值
因而無法保證整過NAT轉化後的資料包攜帶的timestamp值嚴格遞增
4. 當伺服器的per-host PAWS機制被觸發後,會丟棄timestamp值不符合遞增條件的資料包

解決辦法就是不建議同時開啟tcp_timestamp和tcp_tw_recycle。
那到底怎麼配置?

開啟tcp_timestamp,但不要開tcp_tw_recycle  
開啟tcp_timestamp,但不要開tcp_tw_recycle  
開啟tcp_timestamp,但不要開tcp_tw_recycle  

因為timestamp有更多其他的作用,而tcp_tw_recycle本身就是依賴於timestamp的。在不開啟timestamp的情況下,單獨開啟tcp_tw_recycle並沒有什麼用
其實上述強調三遍的配置,正是目前Linux的預設配置。所以說啊,不真正搞懂核心的引數選項,就不要盲目修改。尤其是在官方文件對tcp_tw_recycle已經強調了不要盲目修改的情況下

那為什麼有人推薦同時開啟tcp_timestamp和tcp_tw_recycle呢?
因為同時開啟後,能夠更快的回收TIME-WAIT狀態的socket    <== 這也正是PAWS從per-conn在配置後擴充套件到per-host的目的  
只可惜邏輯是對的,但是沒有考慮到公網廣泛存在的NAT機制可能帶來的問題。  

原始碼細節分析

這部分是linux 3.10原始碼部分的分析,算是對於以上理論分析提供的依據,不關係細節的話可以忽略本節

// tcp_v4_conn_request(), net/ipv4/tcp_ipv4.c line 1551
if (tmp_opt.saw_tstamp &&      // 是否見到過tcp_timestamp選項
    tcp_death_row.sysctl_tw_recycle &&   // 接著判斷是否開啟recycle
    (dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&    // 最終判斷saddr是否有相關記錄在route表中
    fl4.daddr == saffr) {
    if (!tcp_peer_is_proven(req, dst, true)) {  // 如果這個建連請求不能被proven,則會被丟棄
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
        goto drop_and_release;
    }
}

// tcp_peer_is_proven() net/ipv4/tcp_metrics.c line 536
// 負責判斷接收到的request請求的timestamp是否符合要求,最重要的一段程式碼如下
if (tm &&
    // 判斷儲存tcpm_ts_stamp值是否有效,TCP_PAWS_MSL=60
    (u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
    // 如果記錄值大於當前收到的req中的timestamp值,則丟棄。TCP_PAWS_WINDOW=1
    (u32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW) {
        ret = false;
}

至此可以看到:在tcp_timestamp和tcp_tw_recycle同時開啟時,會觸發Linux的per-host的PAWS機制

接下來分析開啟tcp_tw_recycle和tcp_timestamp時,是怎麼快速回收TIME-WAIT的

// tcp_time_wait() net/ipv4/tcp_minisocks.c  line 267
...
// ts_recent_stamp依賴於timestamp選項的開啟,可進tcp_minisocks.c驗證  
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
    recycle_ok = tcp_remember_stamp(s);
...
// 如果能夠recycle,則使用更短的rto作為timeout,從而更快回收TIME-WAIT
if (timeo < rto)
    timeo = rto;
if (recycle_ok) {
    tw->tw_timeout = rto;
} else {
    tw->tw_timeout = TCP_TIMEWAIT_LEN;
    if (state == TCP_TIME_WAIT) 
        timeo = TCP_TIMEWAIT_LEN;    
}
inet_twsh_schedule(tw, &tcp_death_row, timeo, TCP_TIMEWAIT_LEN);

// tcp_timewait_state_process() net/ipv4/tcp_minisocks.c line 94
// 另一條進入time-wait的路線有類似的程式碼
if (tcp_death_row.sysctl_tw_recycle &&
    tcptw->tw_ts_recent_stamp &&
    tcp_tw_remember_stamp(tw))
        inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                           TCP_TIMEWAIT_LEN);
else
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN);

參考資料

Documentation: ip-sysctl.txt
RFC 1323: TCP Extensions for High Performance

相關文章