Python實現簡單的udp打洞(P2P)
UDP穿越NAT的具體設計
首先,Client A登入伺服器,NAT 1為這次的Session分配了一個埠60000,那麼Server S收到的Client A的地址是200.0.0.132:60000,這就是ClientA的外網地址了。同樣,Client B登入Server S,NAT B給此次Session分配的埠是40000,那麼Server S收到的B的地址是200.0.0.133:40000。
此時,Client A與Client B都可以與ServerS通訊了。如果Client A此時想直接傳送資訊給ClientB,那麼他可以從Server S那兒獲得B的公網地址200.0.0.133:40000,在雙方都是FullCone NAT的情況下,Client A就能夠直接往ClientB的公網IP:Port傳送資料了。
總結一下這個過程:如果ClientA想向Client B傳送資訊,那麼雙方都需要在Server上通訊一次做登記,然後將Client B的公網地址和埠傳送給ClientA,最好Client A直接連線ClientB的公網地址和埠。呵呵,是不是很繞口,不過沒關係,想一想就很清楚了。
注意:以上過程只適合於ConeNAT的情況,如果是Symmetric NAT,那麼當Client A向Client B在NAT裝置上沒有形成一個Session,而只有和ClientB 連線Server的Session,所以NAT裝置是會對包進行丟棄的。不過如何Symmetric NAT的埠是按照順序進行開啟的話,那可以通過Server命令Client B傳送資料給ClientA的IP:Port,然後Client A通過線性埠掃描對埠進行猜測。不過這個方式存在失敗率,而且不能明確瞭解NAT裝置開啟的埠的規律,所以沒有去實現。
四種型別的NAT介紹
NAT的分類:在STUN協議中,根據內部終端的地址(LocalIP:LocalPort)到NAT出口的公網地址(PublicIP:PublicPort)的影射方式,把NAT分為四種型別(英文原文詳見rfc3489標準:http://www.ietf.org/rfc/rfc3489.txt): |
1.Full Cone
這種NAT內部的機器A連線過外網機器C後,NAT會開啟一個埠,然後外網的任何發到這個開啟的埠的UDP資料包都可以到達A.不管是不是C發過來的。
2. Restricted Cone
這種NAT內部的機器A連線過外網的機器C後,NAT開啟一個埠,然後C可以用任何埠和A通訊,其他的外網機器不行。
3. Port Restricted Cone
這種NAT內部的機器A連線過外網的機器C後,NAT開啟一個埠,然後C可以用原來的埠和A通訊,其他的外網機器不行。
4.Symmetic
對於這種NAT.連線不同的外部目標。原來NAT開啟的埠會變化,而Cone NAT不會,雖然可以用埠猜測,但是成功的概率很小,因此放棄這種NAT的UDP打洞。
不同NAT環境下UDP穿透可行性分析
兩側NAT屬於Full Cone NAT
則無論A側NAT屬於Cone NAT還是SymmetricNAT,包都能順利到達B。如果P2P程式設計得好,使得B主動到A的包也能借用A主動發起建立的通道的話,則即使A側NAT屬於Symmetric NAT,B發出的包也能順利到達A。
B側NAT屬於Restricted Cone或PortRestricted Cone
則包不能到達B。再細分兩種情況
(1)、A側NAT屬於RestrictedCone或Port Restricted Cone
雖然先前那個初始包不曾到達B,但該發包過程已經在A側NAT上留下了足夠的記錄。如果在這個記錄沒有超時之前,B也重複和A一樣的動作,即向A發包,雖然A側NAT屬於RestrictedCone或Port Restricted Cone,但先前A側NAT已經認為A已經向B發過包,故B向A發包能夠順利到達A。同理,此後A到B的包,也能順利到達。
(2)、A側NAT屬於SymmetricNAT
因為A側NAT屬於Symmetric NAT,且最初A到Server發包的過程在A側NAT留下了記錄,故A到B發包過程在A側NAT上留下的記錄埠產生了變化。而B向A的發包,只能根據Sever給他的關於A的資訊,發往A,因為A埠受限,故此路不通。再來看B側NAT,由於B也向A發過了包,且B側NAT屬於Restricted Cone或Port Restricted Cone,故在B側NAT上留下的記錄,此後,如果A還繼續向B發包的話(因為同一目標,故仍然使用前面的對映),如果B側NAT屬於Restricted Cone,則從A(210.21.12.140:8001)來的包能夠順利到達B;如果B側NAT屬於PortRestricted Cone,則包永遠無法到達B。
結論1:只要單側NAT屬於Full Cone NAT,即可實現雙向通訊。
結論2:只要兩側NAT都不屬於Symmetric NAT,也可雙向通訊。換種說法,只要兩側NAT都屬於Cone NAT,即可雙向通訊。
結論3:一側NAT屬於Symmetric NAT,另一側NAT屬於Restricted Cone,也可雙向通訊。
結論4,兩個都是Symmetric NAT或者一個是SymmetricNAT、另一個是Port Restricted Cone,則不能雙向通訊。
常見的NAT實現
由於存在有4種NAT的型別,所以在方案設計中,必須要考慮到用哪種軟體或者裝置來模擬NAT環境。我在實現過程中嘗試過了很多選擇,包括Linux下的iptables,海蜘蛛的軟路由,使用CISCO的IOS模擬,window2003自帶的NAT服務,還有VMWARE自帶的NAT網路環境。最終發現window2003和VMWARE是支援full cone nat的。 |
iptables
通過前人的試驗和我自己的驗證,iptables確實是貨真價實的Symmetric NAT。不過也有人通過改寫在Linux2.4核心下的iptables原始碼將SymmetricNAT改為了Full Cone NAT,也可以通過編寫規則作弊的方式實現full nat cone,不過我最終沒有采取iptables。一是改寫原始碼的方式比較繁瑣,需要修改很多個原始碼檔案。而使用編寫規則作弊的方式也覺得有點自欺欺人了。
window2003
Window 2003自帶有路由和遠端訪問服務,其中包含有NAT服務。經過測試,可以實現完全的Full Cone NAT。
ciscoIOS模擬
我曾使用c3640-is-mz.122-27版本的IOS,通過Dynamips模擬路由器配置NAT,發現不支援Full Cone NAT。可能有老版本的IOS會有支援,不過我沒有一一測試。
Server端程式碼:
#!/usr/bin/python
#coding:utf-8
import socket, sys, SocketServer, threading, thread, time
SERVER_PORT = 1234
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((”, SERVER_PORT))
user_list = []
def server_handle():
whileTrue:
cli_date, cli_pub_add = sock.recvfrom(8192)
now_user = []
headder = []
cli_str = {}
headder = cli_date.split(‘\t’)
for one_line in headder:
str = {}
str = one_line
args = str.split(‘:’)
cli_str[args[0]] = args[1]
if cli_str[‘type’] == ‘login’ :
del cli_str[‘type’]
now_user = cli_str
now_user[‘cli_pub_ip’] = cli_pub_add[0]
now_user[‘cli_pub_port’] = cli_pub_add[1]
user_list.append(now_user)
toclient = ‘info#%s login in successful , the info from server’%now_user[‘user_name’]
sock.sendto(toclient,cli_pub_add)
print’-‘*100
print”%s 已經登入,公網IP:%s 埠:%d\n”%(now_user[‘user_name’],now_user[‘cli_pub_ip’],now_user[‘cli_pub_port’])
print”以下是已經登入的使用者列表”
for one_user in user_list:
print’使用者名稱:%s 公網ip:%s 公網埠:%s 私網ip:%s 私網埠:%s’%(one_user[‘user_name’],one_user[‘cli_pub_ip’],one_user[‘cli_pub_port’],one_user[‘private_ip’],one_user[‘private_port’])
elif cli_str[‘type’] == ‘alive’:
pass
elif cli_str[‘type’] == ‘logout’ :
pass
elif cli_str[‘type’] == ‘getalluser’ :
print’-‘*100
for one_user in user_list :
toclient = ‘getalluser#username:%s pub_ip:%s pub_port:%s pri_ip:%s pri_port:%s’%(one_user[‘user_name’],one_user[‘cli_pub_ip’],one_user[‘cli_pub_port’],one_user[‘private_ip’],one_user[‘private_port’])
sock.sendto(toclient,cli_pub_add)
if __name__ == ‘__main__’:
thread.start_new_thread(server_handle, ())
print’伺服器程式已啟動,等待客戶連線’
whileTrue:
for one_user in user_list:
toclient = ‘keepconnect#111’
sock.sendto(toclient,(one_user[‘cli_pub_ip’],one_user[‘cli_pub_port’]))
time.sleep(1)
Client端程式碼:
#!/usr/bin/python
#coding:utf-8
import socket, SocketServer, threading, thread, time
CLIENT_PORT = 4321
SERVER_IP = “200.0.0.128”
SERVER_PORT = 1234
user_list = {}
local_ip = socket.gethostbyname(socket.gethostname())
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def server_handle():
print’客戶端執行緒已經啟動 , 等待其它客戶端連線’
whileTrue:
data, addr = sock.recvfrom(8192)
data_str = data.split(‘#’)
data_type = data_str[0]
data_info = data_str[1]
if data_type == ‘info’ :
del data_str[0]
print data_info
if data_type == ‘getalluser’ :
data_sp = data_info.split(’ ‘)
user_name = data_sp[0].split(‘:’)[1]
del data_sp[0]
user_list[user_name] = {}
for one_line in data_sp:
arg = one_line.split(‘:’)
user_list[user_name][arg[0]] = arg[1]
if data_type == ‘echo’ :
print data_info
if data_type == ‘keepconnect’:
messeg = ‘type:alive’
sock.sendto(messeg, addr)
if __name__ == ‘__main__’:
thread.start_new_thread(server_handle, ())
time.sleep(0.1)
cmd = raw_input(‘輸入指令>>’)
whileTrue:
args = cmd.split(’ ‘)
if args[0] == ‘login’:
user_name = args[1]
local_uname = args[1]
address = “private_ip:%s private_port:%d” % (local_ip, CLIENT_PORT)
headder = “type:login\tuser_name:%s\tprivate_ip:%s\tprivate_port:%d” % (user_name,local_ip,CLIENT_PORT)
sock.sendto(headder, (SERVER_IP, SERVER_PORT))
elif args[0] == ‘getalluser’:
headder = “type:getalluser\tuser_name:al”
sock.sendto(headder,(SERVER_IP,SERVER_PORT))
print’獲取使用者列表中。。。’
time.sleep(1)
for one_user in user_list:
print’username:%s pub_ip:%s pub_port:%s pri_ip:%s pri_port:%s’%(one_user,user_list[one_user][‘pub_ip’],user_list[one_user][‘pub_port’],user_list[one_user][‘pri_ip’],user_list[one_user][‘pri_port’])
elif args[0] == ‘connect’:
user_name = args[1]
to_user_ip = user_list[user_name][‘pub_ip’]
to_user_port = int(user_list[user_name][‘pub_port’])
elif args[0] ==’echo’:
m = ’ ‘.join(args[1:])
messeg = ‘echo#from %s:%s’%(local_uname,m)
sock.sendto(messeg, (to_user_ip, to_user_port))
time.sleep(0.1)
cmd = raw_input(‘輸入指令>>’)
相關文章
- UDP內網穿透和打洞原理的C語言程式碼實現UDP內網穿透C語言
- python實現的簡單點對點(p2p)聊天Python
- Java用UDP實現簡單聊天JavaUDP
- 聊聊UDP、TCP和實現一個簡單的JAVA UDP小DemoUDPTCPJava
- python 實現 TCP、UDP 客戶端最簡流程PythonTCPUDP客戶端
- TCP和UDP實現簡單一對一通訊TCPUDP
- python下ssh的簡單實現Python
- python實現簡單猜單詞遊戲Python遊戲
- Python實現UDP包程式設計PythonUDP程式設計
- Python實現簡單的excel對比工具PythonExcel
- Python實現簡單負載均衡Python負載
- SwiftNIO初探-簡單UDP通訊SwiftUDP
- python 爬取 blessing skin 的簡單實現Python
- Python技法:實現簡單的遞迴下降ParserPython遞迴
- Python實現簡單驗證碼的轉文字Python
- 用python實現簡單的線上翻譯程式Python
- Python使用TCP實現簡單對話PythonTCP
- python linkedhashset 簡單實現,使用OrderedDictPython
- 實現簡單的BitMap
- ArrayList的簡單實現
- AOP的簡單實現
- 簡單的 HashMap 實現HashMap
- 簡單的Filter實現Filter
- Promise的簡單實現Promise
- Kafka 簡單實驗二(Python實現簡單生產者消費者)KafkaPython
- 簡單介紹python中的單向連結串列實現Python
- 如何用Python實現多工版的udp聊天器PythonUDP
- TCP、UDP、HTTP及Socket的簡單講解TCPUDPHTTP
- TCP/UDP簡單介紹及JavaSocket的使用TCPUDPJava
- 使用 python 實現簡單的共享鎖和排他鎖Python
- 簡單的python程式碼實現語音朗讀Python
- python爬蟲簡單實現逆向JS解密Python爬蟲JS解密
- Python Django實現簡單購物車功能PythonDjango
- [python]有限狀態機(FSM)簡單實現Python
- Python簡單實現多執行緒例子Python執行緒
- python如何實現簡單的爬蟲功能?Python學習教程!Python爬蟲
- 簡單的實現vue原理Vue
- 簡單的實現React原理React