https://plantegg.github.io/2020/11/18/TCP%E8%BF%9E%E6%8E%A5%E4%B8%BA%E5%95%A5%E4%BA%92%E4%B8%B2%E4%BA%86/
背景
應用每過一段時間總是會拋出幾個連接異常的錯誤,需要查明原因。
排查後發現是TCP連接互串了,這個案例實在是很珍惜,所以記錄一下。
抓包
業務結構: 應用->MySQL(10.112.61.163)
在 應用 機器上抓包這個異常連接如下(3269爲MySQL服務端口):
粗一看沒啥奇怪的,就是應用發查詢給3269,但是一直沒收到3269的ack,所以一直重傳。這裏唯一的解釋就是網絡不通。最後MySQL的3269還回復了一個rst,這個rst的id是42889,引起了我的好奇,跟前面的16439不連貫,正常應該是16440纔對。(請記住上圖中的綠框中的數字)
於是我過濾了一下端口61902上的所有包:
可以看到綠框中的查詢從61902端口發給3269後,很奇怪居然收到了一個來自別的IP+3306端口的reset,這個包對這個連接來說自然是不認識(這個連接只接受3269的回包),就扔掉了。但是也沒收到3269的ack,所以只能不停地重傳,然後每次都收到3306的reset,reset包的seq、id都能和上圖的綠框對應上。
明明他們應該是兩個連接:
61902->10.141.16.0:3306
61902->10.112.61.163:3269
他們雖然用的本地ip端口(61902)是一樣的, 但是根據四元組不一樣,還是不同的TCP連接,所以應該是不會互相干擾的。但是實際看起來seq、id都重複了,不會有這麼巧,非常像是TCP互串了。
分析原因
10.141.16.0 這個ip看起來像是lvs的ip,查了一下系統,果然是lvs,然後這個lvs 後面的rs就是10.112.61.163
那麼這個連結構就是10.141.16.0:3306:
應用 -> lvs(10.141.16.0:3306)-> 10.112.61.163:3269 跟應用直接連MySQL是一回事了
所以這裏的疑問就變成了:10.141.16.0 這個IP的3306端口爲啥能知道 10.112.61.163:3269端口的seq和id,也許是TCP連接串了
接着往下排查
先打個岔,分析下這裏的LVS的原理
這裏使用的是 full NAT模型(full NetWork Address Translation-全部網絡地址轉換)
基本流程(類似NAT):
- client發出請求(sip 200.200.200.2 dip 200.200.200.1)
- 請求包到達lvs,lvs修改請求包爲(sip 200.200.200.1, dip rip) 注意這裏sip/dip都被修改了
- 請求包到達rs, rs回覆(sip rip,dip 200.200.200.1)
- 這個回覆包的目的IP是VIP(不像NAT中是 cip),所以LVS和RS不在一個vlan通過IP路由也能到達lvs
- lvs修改sip爲vip, dip爲cip,修改後的回覆包(sip 200.200.200.1,dip 200.200.200.2)發給client
注意上圖中綠色的進包和紅色的出包他們的地址變化
本來這個模型下都是正常的,但是爲了Real Server能拿到client ip,也就是Real Server記錄來源ip的時候希望記錄的是client ip而不是LVS ip。這個時候LVS會將client ip放在tcp的options裏面,然後在RealServer機器的內核裏面將options中的client ip取出替換掉 lvs ip。所以Real Server上感知到的對端ip就是client ip。
回包的時候RealServer上的內核模塊同樣將目標地址從client ip改成lvs ip,同時將client ip放入options中。
回到問題
看完理論,再來分析這兩個連接的行爲
fulnat模式下連接經過lvs到達mysql後,mysql上看到的連接信息是,cip+port,也就是在MySQL上的連接
lvs-ip:port -> 10.112.61.163:3269 被修改成了 client-ip:61902 **-> 10.112.61.163:3269
那麼跟不走LVS的連接:
client-ip:61902 -> 10.112.61.163:3269 (直連) 完全重複了。
MySQL端看到的兩個連接四元組一模一樣了:
10.112.61.163:3269 -> client-ip:61902 (走LVS,本來應該是lvs ip的,但是被替換成了client ip)
10.112.61.163:3269 -> client-ip:61902 (直連)
這個時候應用端看到的還是兩個連接:
client-ip:61902 -> 10.141.16.0:3306 (走LVS)
client-ip:61902 -> 10.112.61.163:3269 (直連)
總結下,也就是這個連接經過LVS轉換後在服務端(MYSQL)跟直連MySQL的連接四元組完全重複了,也就是MySQL會認爲這兩個連接就是同一個連接,所以必然出問題了。
實際兩個連接建立的情況:
和mysqlserver的61902是04:22建起來的,和lvs的61902端口 是42:10建起來的,和lvs的61902建起來之後馬上就出問題了
問題出現的條件
- fulnat模式的LVS,RS上裝有slb_toa內核模塊(RS上會將LVS ip還原成client ip)
- client端正好重用一個相同的本地端口分別和RS以及LVS建立了兩個連接
這個時候這兩個連接在MySQL端就會變成一個,然後兩個連接的內容互串,必然導致rst
這個問題還挺有意思的,估計沒幾個程序員一輩子能碰上一次。推薦另外一個好玩的連接:如何創建一個自己連自己的TCP連接
參考資料
https://idea.popcount.org/2014-04-03-bind-before-connect/
另一種形式的tcp連接互串,新連接重用了time_wait的port,導致命中lvs內核表中的維護的舊連接發給了老的realserver