TCP打洞技术
1 套接字和TCP端口的重用
實(shí)現(xiàn)基于TCP協(xié)議的p2p“打洞”過(guò)程中,最主要的問(wèn)題不是來(lái)自于TCP協(xié)議,而是來(lái)自于來(lái)自于應(yīng)用 程序的API接口。這是由于標(biāo)準(zhǔn)的伯克利(Berkeley)套接字的API是圍繞著構(gòu)建客戶端/服務(wù)器程序 而設(shè)計(jì)的,API允許TCP流套接字通過(guò)調(diào)用connect()函數(shù)來(lái)建立向外的連接,或者通過(guò)listen()和 accept函數(shù)接受來(lái)自外部的連接,但是,API不提供類似UDP那樣的,同一個(gè)端口既可以向外連接, 又能夠接受來(lái)自外部的連接。而且更糟的是,TCP的套接字通常僅允許建立1對(duì)1的響應(yīng),即應(yīng)用程 序在將一個(gè)套接字綁定到本地的一個(gè)端口以后,任何試圖將第二個(gè)套接字綁定到該端口的操作都會(huì) 失敗。
為了讓TCP“打洞”能夠順利工作,我們需要使用一個(gè)本地的TCP端口來(lái)監(jiān)聽(tīng)來(lái)自外部的TCP連接,同時(shí) 建立多個(gè)向外的TCP連接。幸運(yùn)的是,所有的主流操作系統(tǒng)都能夠支持特殊的TCP套接字參數(shù),通常 叫做“SO_REUSEADDR”,該參數(shù)允許應(yīng)用程序?qū)⒍鄠€(gè)套接字綁定到本地的一個(gè)endpoint(只要所有要 綁定的套接字都設(shè)置了SO_REUSEADDR參數(shù)即可)。BSD系統(tǒng)引入了SO_REUSEPORT參數(shù),該參數(shù)用于區(qū)分 端口重用還是地址重用,在這樣的系統(tǒng)里面,上述所有的參數(shù)必須都設(shè)置才行。
2 打開(kāi)p2p的TCP流
假定客戶端A希望建立與B的TCP連接。我們像通常一樣假定A和B已經(jīng)與公網(wǎng)上的已知服務(wù)器S建立了TCP 連接。服務(wù)器記錄下來(lái)每個(gè)聯(lián)入的客戶端的公網(wǎng)和內(nèi)網(wǎng)的endpoints,如同為UDP服務(wù)的時(shí)候一樣。 從協(xié)議層來(lái)看,TCP“打洞”與UDP“打洞”是幾乎完全相同的過(guò)程。
1)、客戶端A使用其與服務(wù)器S的連接向服務(wù)器發(fā)送請(qǐng)求,要求服務(wù)器S協(xié)助其連接客戶端B。 2)、S將B的公網(wǎng)和內(nèi)網(wǎng)的TCP endpoint返回給A,同時(shí),S將A的公網(wǎng)和內(nèi)網(wǎng)的endpoint發(fā)送給B。 3)、客戶端A和B使用連接S的端口異步地發(fā)起向?qū)Ψ降墓W(wǎng)、內(nèi)網(wǎng)endpoint的TCP連接,同時(shí)監(jiān)聽(tīng) 各自的本地TCP端口是否有外部的連接聯(lián)入。 4)、A和B開(kāi)始等待向外的連接是否成功,檢查是否有新連接聯(lián)入。如果向外的連接由于某種網(wǎng)絡(luò) 錯(cuò)誤而失敗,如:“連接被重置”或者“節(jié)點(diǎn)無(wú)法訪問(wèn)”,客戶端只需要延遲一小段時(shí)間(例如 延遲一秒鐘),然后重新發(fā)起連接即可,延遲的時(shí)間和重復(fù)連接的次數(shù)可以由應(yīng)用程序編寫(xiě)者 來(lái)確定。 5)、TCP連接建立起來(lái)以后,客戶端之間應(yīng)該開(kāi)始鑒權(quán)操作,確保目前聯(lián)入的連接就是所希望的 連接。如果鑒權(quán)失敗,客戶端將關(guān)閉連接,并且繼續(xù)等待新的連接聯(lián)入。客戶端通常采用 “先入為主”的策略,只接受第一個(gè)通過(guò)鑒權(quán)操作的客戶端,然后將進(jìn)入p2p通信過(guò)程不再繼續(xù) 等待是否有新的連接聯(lián)入。
與UDP不同的是,使用UDP協(xié)議的每個(gè)客戶端只需要一個(gè)套接字即可完成與服務(wù)器S通信, 并同時(shí)與多個(gè)p2p客戶端通信的任務(wù),而TCP客戶端必須處理多個(gè)套接字綁定到同一個(gè)本地 TCP端口的問(wèn)題,如圖7所示。
現(xiàn)在來(lái)看更加實(shí)際的一種情景,A與B分別位于不同的NAT設(shè)備后面,如圖5所示,并且假定圖中 的端口號(hào)是TCP協(xié)議的端口號(hào),而不是UDP的端口號(hào)。圖中向外的連接代表A和B向?qū)Ψ降膬?nèi)網(wǎng) endpoint發(fā)起的連接,這些連接或許會(huì)失敗或者無(wú)法連接到對(duì)方。如同使用UDP協(xié)議進(jìn)行“打洞” 操作遇到的問(wèn)題一樣,TCP的“打洞”操作也會(huì)遇到內(nèi)網(wǎng)的IP與“偽”公網(wǎng)IP重復(fù)造成連接失敗或者 錯(cuò)誤連接之類的問(wèn)題。
客戶端向彼此公網(wǎng)endpoint發(fā)起連接的操作,會(huì)使得各自的NAT設(shè)備打開(kāi)新的“洞”允許A與B的 TCP數(shù)據(jù)通過(guò)。如果NAT設(shè)備支持TCP“打洞”操作的話,一個(gè)在客戶端之間的基于TCP協(xié)議的流 通道就會(huì)自動(dòng)建立起來(lái)。如果A向B發(fā)送的第一個(gè)SYN包發(fā)到了B的NAT設(shè)備,而B(niǎo)在此前沒(méi)有向 A發(fā)送SYN包,B的NAT設(shè)備會(huì)丟棄這個(gè)包,這會(huì)引起A的“連接失敗”或“無(wú)法連接”問(wèn)題。而此時(shí), 由于A已經(jīng)向B發(fā)送過(guò)SYN包,B發(fā)往A的SYN包將被看作是由A發(fā)往B的包的回應(yīng)的一部分, 所以B發(fā)往A的SYN包會(huì)順利地通過(guò)A的NAT設(shè)備,到達(dá)A,從而建立起A與B的p2p連接。
3 從應(yīng)用程序的角度來(lái)看TCP“打洞”
從應(yīng)用程序的角度來(lái)看,在進(jìn)行TCP“打洞”的時(shí)候都發(fā)生了什么呢?假定A首先向B發(fā)出SYN包, 該包發(fā)往B的公網(wǎng)endpoint,并且被B的NAT設(shè)備丟棄,但是B發(fā)往A的公網(wǎng)endpoint的SYN包則 通過(guò)A的NAT到達(dá)了A,然后,會(huì)發(fā)生以下的兩種結(jié)果中的一種,具體是哪一種取決于操作系統(tǒng) 對(duì)TCP協(xié)議的實(shí)現(xiàn):
(1)A的TCP實(shí)現(xiàn)會(huì)發(fā)現(xiàn)收到的SYN包就是其發(fā)起連接并希望聯(lián)入的B的SYN包,通俗一點(diǎn)來(lái)說(shuō) 就是“說(shuō)曹操,曹操到”的意思,本來(lái)A要去找B,結(jié)果B自己找上門來(lái)了。A的TCP協(xié)議棧因此 會(huì)把B做為A向B發(fā)起連接connect的一部分,并認(rèn)為連接已經(jīng)成功。程序A調(diào)用的異步connect() 函數(shù)將成功返回,A的listen()等待從外部聯(lián)入的函數(shù)將沒(méi)有任何反映。此時(shí),B聯(lián)入A的操作 在A程序的內(nèi)部被理解為A聯(lián)入B連接成功,并且A開(kāi)始使用這個(gè)連接與B開(kāi)始p2p通信。
由于收到的SYN包中不包含A需要的ACK數(shù)據(jù),因此,A的TCP將用SYN-ACK包回應(yīng)B的公網(wǎng)endpoint, 并且將使用先前A發(fā)向B的SYN包一樣的序列號(hào)。一旦B的TCP收到由A發(fā)來(lái)的SYN-ACK包,則把自己 的ACK包發(fā)給A,然后兩端建立起TCP連接。簡(jiǎn)單的說(shuō),第一種,就是即使A發(fā)往B的SYN包被B的NAT 丟棄了,但是由于B發(fā)往A的包到達(dá)了A。結(jié)果是,A認(rèn)為自己連接成功了,B也認(rèn)為自己連接成功 了,不管是誰(shuí)成功了,總之連接是已經(jīng)建立起來(lái)了。
(2)另外一種結(jié)果是,A的TCP實(shí)現(xiàn)沒(méi)有像(1)中所講的那么“智能”,它沒(méi)有發(fā)現(xiàn)現(xiàn)在聯(lián)入的B 就是自己希望聯(lián)入的。就好比在機(jī)場(chǎng)接人,明明遇到了自己想要接的人卻不認(rèn)識(shí),誤認(rèn)為是其它 的人,安排別人給接走了,后來(lái)才知道是自己錯(cuò)過(guò)了機(jī)會(huì),但是無(wú)論如何,人已經(jīng)接到了任務(wù) 已經(jīng)完成了。然后,A通過(guò)常規(guī)的listen()函數(shù)和accept()函數(shù)得到與B的連接,而由A發(fā)起的向 B的公網(wǎng)endpoint的連接會(huì)以失敗告終。盡管A向B的連接失敗,A仍然得到了B發(fā)起的向A的連接, 等效于A與B之間已經(jīng)聯(lián)通,不管中間過(guò)程如何,A與B已經(jīng)連接起來(lái)了,結(jié)果是A和B的基于TCP協(xié)議 的p2p連接已經(jīng)建立起來(lái)了。
第一種結(jié)果適用于基于BSD的操作系統(tǒng)對(duì)于TCP的實(shí)現(xiàn),而第二種結(jié)果更加普遍一些,多數(shù)linux和 windows系統(tǒng)都會(huì)按照第二種結(jié)果來(lái)處理。
總結(jié)
- 上一篇: WSAAccept()函数使用解析
- 下一篇: UDP用打洞技术穿透NAT的原理与实现