詳細解讀NAT和內網穿透


詳細解讀NAT和內網穿透

0.前言

可能是因為之前折騰過搭網站之類的事情的原因,我個人對計算機網絡比較感興趣。半年之前試過花生殼的內網穿透服務之后寫過一篇文章談內網穿透的原理,但是最近發給同學看時候他說打不開,才知道被博客園和諧了,趁機回顧再看才發現之前寫的內容也有問題(那時候還沒有學計算機網絡),所以今天又參考了許多資料,力圖正確、准確地解讀一下NAT和內網穿透,這兩個隨着ipv6的普及很可能會被淘汰、但是我個人覺得短時間不可能完全淘汰的技術。

1.NAT-從實際案例講起

用我自己的情況做個例子吧,我的網絡情況是:

  1. 一棟樓有一個電信的公網ip,綁定到一台提供NAT服務的路由器上
  2. 每層樓有一個交換機,與1中所說的路由器連接
  3. 交換機使用不同的端口連接每個房間的路由器
  4. 房間的路由器又組建了設備間的局域網

我來詳細描述一下我的設備和一台公網ip下的服務器通信的過程,也算是復習計算機網絡了吧,這里假設我的電腦已經知道目標設備的ip地址了(也就是不算DNS部分了):

  1. 設備發送數據包,源ip是房間路由器創建的內網下的ip,端口號是socket隨機選擇的,其他的目標ip、源mac都是確定的,目標mac填的是自己路由器的mac地址

    設備會用本機配置的24位子網掩碼與目標地址進行“與”運算,得出目標地址與本機不是同一網段,因此發送目標的數據包需要經過路由器的轉發。 -[4]

  2. 路由器先把源ip地址改成自己在樓中使用的那個內網ip,為設備分配一個唯一的端口號,並在路由表中記錄下這一映射關系。根據路由表中的記錄(確定要訪問的地址不是樓里的地址,是公網的地址)獲取下一跳的ip地址, 並根據ARP協議確定下一跳(也就是每層的交換機)的MAC地址 ,將目標mac地址修改為下一跳的mac地址,然后在數據鏈路層和網絡層中進行傳輸

  3. 傳到每一層的交換機時,不需要修改源ip地址(因為是交換機,沒有修改Ip的功能),只把目標mac改為NAT路由器的地址,然后再通過數據鏈路層和網絡層傳輸

  4. 到了NAT服務器,服務器隨機為其分配一個端口[3],並在Track Table中保存這個[內網ip:端口號->目標ip:端口號]的映射,這一過程稱為連接跟蹤。注意此時的內網ip指的是路由器的ip,端口號是路由器為設備分配的端口號

  5. 服務器收到數據包,再向NAT路由器返回數據包,NAT路由器通過查詢Track table確定內網ip(是路由器的內網ip)和端口號,並改變目標Ip為此,返回發送給每層樓的交換機,交換機通過arp協議借助路由器的ip確定路由器的mac地址,並將目標mac地址改為此,再發送給路由器

  6. 路由器通過端口號確定是哪一台內部設備,轉發給該設備

這里對於NAT,關鍵的一步是4,NAT只會接受在Track table中有ip和端口記錄的外部訪問,其他的都一概不轉發,這也就是我們常說的NAT只能內網訪問外網,不能外網訪問內網

2.內網穿透

但是不得不說,P2P的需求是真實存在的,為了解決NAT帶來的問題,內網穿透誕生了。

這篇文章里比較好地提到了內網穿透的原理,現摘錄出來:

假設現在有內網客戶端A和內網客戶端B,有公網服務端S。
如果A和B想要進行UDP通信,則必須穿透雙方的NAT路由。假設為NAT-A和NAT-B。

A發送數據包到公網S,B發送數據包到公網S,則S分別得到了A和B的公網IP,
S也和A B 分別建立了會話,由S發到NAT-A的數據包會被NAT-A直接轉發給A,
由S發到NAT-B的數據包會被NAT-B直接轉發給B,除了S發出的數據包之外的則會被丟棄。
所以:現在A B 都能分別和S進行全雙工通訊了,但是A B之間還不能直接通訊。

解決辦法是:A向B的公網IP發送一個數據包,則NAT-A能接收來自NAT-B的數據包
並轉發給A了(即B現在能訪問A了);再由S命令B向A的公網IP發送一個數據包,則
NAT-B能接收來自NAT-A的數據包並轉發給B了(即A現在能訪問B了)。

以上就是“打洞”的原理。

為了保證A的路由器有與B的session,A要定時與B做心跳包,同樣,B也要定時與A做心跳,這樣,雙方的通信通道都是通的,就可以進行任意的通信了。

圖解如下:

img

上面說的就是UDP打洞的原理,但是為什么是UDP呢?

UDP的socket允許多個socket綁定到同一個本地端口,而TCP的socket則不允許。
這是這樣一個意思:A B要連接到S,肯定首先A B雙方都會在本地創建一個socket,
去連接S上的socket。創建一個socket必然會綁定一個本地端口(就算應用程序里面沒寫
端口,實際上也是綁定了的,至少java確實如此),假設為8888,這樣A和B才分別建立了到
S的通信信道。接下來就需要打洞了,打洞則需要A和B分別發送數據包到對方的公網IP。但是
問題就在這里:因為NAT設備是根據端口號來確定session,如果是UDP的socket,A B可以
分別再創建socket,然后將socket綁定到8888,這樣打洞就成功了。但是如果是TCP的
socket,則不能再創建socket並綁定到8888了,這樣打洞就無法成功。

道理的確是這么個道理,但是博主說的還不夠清楚,我再解讀下,就用上面原博主給的例子了:

由於NAT的外部端口是隨機指定的,如果A和B分別和服務器通信,都使用8888端口的話,如果A要和B直接打洞且不用8888端口,就會遇到一個問題:A不知道B將來包發出來NAT-B會給它分配什么接口,所以A就沒辦法指定目標Ip的端口號(因為NAT-B是隨機分配端口的,B即使知道了A用了哪個端口打洞也沒辦法讓NAT-B去使用這個特定的端口。綜上,A和B能使用的,只有和服務器連接時已經創建在track table中的那個端口,也就是我們例子中的8888了。

注意track table中的記錄是有有效期的,由於我們不知道外部設備什么時候會訪問我們在內網中的設備,所以我們需要保證設備和服務器之間的連接不能斷開,track table中的映射不能被銷毀,所以需要在一定的時間間隔之后發包來維持NAT中的映射關系,這就是為什么我們用花生殼的時候它一直要求我們“保持在線”的原因了

但是TCP也不是不能進行穿透,這就需要用到端口重用了:

tcp打洞也需要NAT設備支持才行。
tcp的打洞流程和udp的基本一樣,但tcp的api決定了tcp打洞的實現過程和udp不一樣。
tcp按cs方式工作,一個端口只能用來connect或listen,所以需要使用端口重用,才能利用本地nat的端口映射關系。(設置SO_REUSEADDR,在支持SO_REUSEPORT的系統上,要設置這兩個參數。)

連接過程:(以udp打洞的第2種情況為例(典型情況))
nat后的兩個peer,A和B,A和B都bind自己listen的端口,向對方發起連接(connect),即使用相同的端口同時連接和等待連接。因為A和B發出連接的順序有時間差,假設A的syn包到達B的nat時,B的syn包還沒有發出,那么B的nat映射還沒有建立,會導致A的連接請求失敗(連接失敗或無法連接,如果nat返回RST或者icmp差錯,api上可能表現為被RST;有些nat不返回信息直接丟棄syn包(反而更好)),(應用程序發現失敗時,不能關閉socket,closesocket()可能會導致NAT刪除端口映射;隔一段時間(1-2s)后未連接還要繼續嘗試);但后發B的syn包在到達A的nat時,由於A的nat已經建立的映射關系,B的syn包會通過A的nat,被nat轉給A的listen端口,從而進去三次握手,完成tcp連接。

另外,NAT還有許多其他功能,更詳細的介紹可以看這篇文章

3.Reference

  1. https://www.zhihu.com/question/37712168
  2. https://www.zhihu.com/question/55015810
  3. https://www.cnblogs.com/dongzhuangdian/p/5105844.html
  4. http://dengqi.blog.51cto.com/5685776/1223132


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM