[原創]分析解決lvs fullnat模式下後端服務器獲取真實IP地址異常問題
摘要
分析解決lvs fullnat模式下少量的請求記錄client IP不是用戶真實的IP地址問題.
原創文章:來自分析lvs fullnat模式下後端服務器獲取真實IP地址異常問題
問題背景
lvs fullnat模式下觀察後端服務器realserver http/https業務運行係統日誌,有時候可以發現有少量的請求記錄的client IP不是用戶真實的IP地址(存在但出現的概率很小,增加了問題排查的難度),而是屬於lvs主機私有的IP地址。關於fullnat的簡介可以參考https://www.baokaijian.com/?tag=fullnat, 這裏借用一下文中的圖片。
問題分析
realserver linux係統屬於用戶自屬管理默認配置MTU=1500、MSS=1460,在TCP建連時可以通過syn+ack報文通告給client,而client端可屬於移動端或PC端MTU、MSS的差異性較大,通過syn報文通告給realserver端,從而建立的TCP連接取兩者之小值做為鏈路的MSS。中間的LVS隻是做連接與報文轉發的負載均衡。
fullnat 模式下獲取真實IP地址的原理:
開源的lvs fullnat代碼與patch可查閱lvs官方網址https://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY
lvs fullnat應用模式通過TCP三次握手的ack,ack+data報文將client端的真實IP地址插入到tcp options中從而帶給後端的realserver。而後端realserver toa內核模塊中通過hook了tcp_v4_syn_recv_sock()函數,然後調用get_toa_data()從tcp_options中取得客戶端的真實IP地址,具體的調用過程下邊分析。
非標準的tcp三次握手情況分析
先來看以下幾個典型的報文序列是否可以建立三次握手:
1. 亂序的情況(第三個ack和第四個ack+data報文亂序)
- 直接使用ack+data報文(或含有push標誌)不存在bare ack報文
3.直接使用fin+ack+data報文( 或含有push標誌)
來點內核TCP代碼中深度分析:
看一下建立TCP連接時兩次經過的tcp_v4_hnd_req函數流程.
syn報文到來時的處理流程:
tcp_v4_rcv() -->tcp_v4_do_rcv()-->tcp_v4_hnd_req()
查找代表客戶端連接的sock結構,如果沒有找到則返回代表服務器的sock結構, 然後在tcp_rcv_state_process()函數中調用tcp_v4_conn_request()創建req_sock請求結構並放入半連接隊列。
握手階段收到第三個ack報文或其他攜帶ACK標誌的報文處理,tcp_v4_hnd_req()函數將返回代表連接請求的sock結構.
tcp_v4_rcv() ->tcp_v4_do_rcv()-->tcp_v4_hnd_req()-->tcp_check_req()-->tcp_v4_syn_recv_sock()
tcp_v4_syn_recv_sock()函數中將對報文序列號判斷,並完成握手,最後將連接請求從半連接請求移到全連接隊列,等待應用層調用accept。
特別注意在tcp_check_req()函數中檢查了clinet端到來報文的序列號
/* RFC793 page 36: "If the connection is in any non-synchronized state ...
* and the incoming segment acknowledges something not yet
* sent (the segment carries an unacceptable ACK) ...
* a reset is sent."
*
* Invalid ACK: reset will be sent by listening socket
*/
if ((flg & TCP_FLAG_ACK) &&// 正常情況下 收包的ack_seq = 初始發送的seq + 1(syn)
(TCP_SKB_CB(skb)->ack_seq != //此報序列號不等於發送初始序列號 tcp_rsk(req)->snt_isn + 1 + tcp_s_data_size(tcp_sk(sk))))
return sk;
結論: 完成握手最後一個clinet端到來報文ack序號必須是client 端tcp連接ISN初始序號+1(沒有檢查seq序列號),所以隻要符合此規則的都可以通過,然後調用調用tcp_v4_syn_recv_sock()從而調用toa模塊取client IP tcp options。再解釋明白點: 非bare ack亂序報文即帶有數據的ack報文,也可以通過。注:至於seq序列號有效性以及影響能不能真正完成連接建立,這是在後續的tcp_ack()中判斷了。
接著tcp_v4_syn_recv_sock()函數中創建代表與client端連接的sock結構(設置此條TCP連接狀態是TCP_SYN_RECV),然後在tcp_rcv_state_process()函數中case TCP_SYN_RECV代碼段中通過tcp_set_state(sk, TCP_ESTABLISHED); 改狀態為ESTABLISHED連接狀態。
經過在toa模塊中探點調試發現:
從realserver端看到大量使用FIN+ACK+PUSH的含數據報文完成三次握手,這時也不能插入toa options(用戶業務很多用http/https這種post請求推數據,realserver收到後會回複response,但若是反向代理的proxy收到後會FIN掉client,同時向後端realserver再發RST報文)
上述圖中因亂序或直接ack+data報文沒超但達到了1444字節(假如toa需要16字節); 使用ack+data報文完成了三次握手, 使lvs層不能插入用戶的IP:PORT
改進建議
官方lvs fullnat代碼:
https://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY
Lvs-fullnat-synproxy.tar.gz 包中包含fullnat 和toa的patch.
1、 lvs代碼tcp_fnat_in_handler()函數中去掉!tcph->fin的條件判斷,可以解決FIN+PUSH+ACK+數據報文中不能攜帶client端ip與端口的缺陷

2、 後端RS建立監聽時直接使用 setsockopt, 將MSS改小(如toa需要16字節則可減小到1444字節), 從而client到來的報文可以有插入clientip:port的空間
3、 (第2條的替代方法但會增加lvs處理的複雜性:
TCP握手階段lvs收到後端RS發送的syn+ack報文時,將其中的MSS改小(如16字節),傳給client, 從而client到來的報文就有了插入clientip:port的空間。
另一種方法雖然client發送來的報文已經達到了client與RS協商的MSS(如1460), 但若lvs與RS之間設置的MTU(如9000)要大於client與LVS之間的值1460, 並且tcp options有足夠的空間可以強製在tcp options中插入clientip:port)
4、以上修改後最後還是不行(可能存在非標準的client tcp處理),建議客戶端程序(特別是手機APP)建立socket時使用 setsockopt 將MSS改小適當的數值(發現存在有部分終端出現通告給其MSS是1444,但仍然發送1460的報文)
備注:
1. tcp options滿載40字節,如果在握手時用戶有插入自己的數據占滿則lvs判斷沒有空間再插入用戶IP了。
- 發現手機終端的MSS值多種多樣,這裏列裏幾個值, 1412 1120 1452 1400 1394 1260 1460
最後更新:2017-07-18 20:36:21