用Python實現簡單的udp打洞(P2P)

   原來是本人的畢業設計。。。如今拿出來共享下。先申明,做者是lust,老師若是在網上搜到這段代碼可不要說個人畢業設計是網上copy滴哈。。。python

   用python作了一個P2P的簡單實現,能夠經過在服務端註冊公網IP和端口後,直接和其它客戶端進行通訊。目前只能在徹底對稱NAT下成功實現服務器


使用vmware模擬了內網、NAT和公網環境測試有效性,最終實現不經過公網服務器的中轉,2個私網用戶進行直接聊天。目前實現的只是聊天功能,在後續的應用用,也能夠實現內網用戶的遠程控制,或者好比在家遠程鏈接公司內部服務器進行遠程辦公等。網絡


 UDP穿NAT的具體設計app


   首先,Client A登陸服務器,NAT 1爲此次的Session分配了一個端口60000,那麼Server S收到的Client A的地址是200.0.0.132:60000,這就是ClientA的外網地址了。一樣,Client B登陸Server SNAT B給這次Session分配的端口是40000,那麼Server S收到的B的地址是200.0.0.133:40000socket

此時,Client AClient B均可以與ServerS通訊了。若是Client A此時想直接發送信息給ClientB,那麼他能夠從Server S那兒得到B的公網地址200.0.0.133:40000,在雙方都是FullCone NAT的狀況下,Client A就可以直接往ClientB的公網IPPort發送數據了。ide

總結一下這個過程:若是ClientA想向Client B發送信息,那麼雙方都須要在Server上通訊一次作登記,而後將Client B的公網地址和端口發送給ClientA,最好Client A直接鏈接ClientB的公網地址和端口。呵呵,是否是很繞口,不過不要緊,想想就很清楚了。測試

注意:以上過程只適合於ConeNAT的狀況,若是是Symmetric NAT,那麼當Client AClient BNAT設備上沒有造成一個Session,而只有和ClientB 鏈接ServerSession,因此NAT設備是會對包進行丟棄的。不過如何Symmetric NAT的端口是按照順序進行開啓的話,那能夠經過Server命令Client B發送數據給ClientAIP:Port,而後Client A經過線性端口掃描對端口進行猜想。不過這個方式存在失敗率,並且不能明確瞭解NAT設備開啓的端口的規律,因此沒有去實現。spa


四種類型的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不會,雖然能夠用端口猜想,可是成功的機率很小,所以放棄這種NATUDP打洞。


不一樣NAT環境下UDP穿透可行性分析


兩側NAT屬於Full Cone NAT


則不管ANAT屬於Cone NAT仍是SymmetricNAT,包都能順利到達B。若是P2P程序設計得好,使得B主動到A的包也能借用A主動發起創建的通道的話,則即便ANAT屬於Symmetric NATB發出的包也能順利到達A

BNAT屬於Restricted ConePortRestricted Cone

則包不能到達B。再細分兩種狀況

1)、ANAT屬於RestrictedConePort Restricted Cone

雖然先前那個初始包未曾到達B,但該發包過程已經在ANAT上留下了足夠的記錄。若是在這個記錄沒有超時以前,B也重複和A同樣的動做,即向A發包,雖然ANAT屬於RestrictedConePort Restricted Cone,但先前ANAT已經認爲A已經向B發過包,故BA發包可以順利到達A。同理,此後AB的包,也能順利到達。

2)、ANAT屬於SymmetricNAT

由於ANAT屬於Symmetric NAT,且最初AServer發包的過程在ANAT留下了記錄,故AB發包過程在ANAT上留下的記錄端口產生了變化。而BA的發包,只能根據Sever給他的關於A的信息,發往A,由於A端口受限,故此路不通。再來看BNAT,因爲B也向A發過了包,且BNAT屬於Restricted ConePort Restricted Cone,故在BNAT上留下的記錄,此後,若是A還繼續向B發包的話(由於同一目標,故仍然使用前面的映射),若是BNAT屬於Restricted Cone,則從A210.21.12.140:8001)來的包可以順利到達B;若是BNAT屬於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實現

因爲存在有4NAT的類型,因此在方案設計中,必需要考慮到用哪一種軟件或者設備來模擬NAT環境。我在實現過程當中嘗試過了不少選擇,包括Linux下的iptables,海蜘蛛的軟路由,使用CISCOIOS模擬,window2003自帶的NAT服務,還有VMWARE自帶的NAT網絡環境。最終發現window2003VMWARE是支持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端代碼:


  
  
  
  
  1. #!/usr/bin/python

  2. #coding:utf-8

  3. import socket, sys, SocketServer, threading, thread, time

  4. SERVER_PORT = 1234

  5. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

  6. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

  7. sock.bind(('', SERVER_PORT))

  8. user_list = []

  9. def server_handle():

  10. whileTrue:

  11.        cli_date, cli_pub_add = sock.recvfrom(8192)

  12.        now_user = []

  13.        headder = []

  14.        cli_str = {}

  15.        headder = cli_date.split('\t')

  16. for one_line in headder:

  17.            str = {}

  18.            str = one_line

  19.            args = str.split(':')

  20.            cli_str[args[0]] = args[1]

  21. if cli_str['type'] == 'login' :

  22. del cli_str['type']

  23.            now_user = cli_str

  24.            now_user['cli_pub_ip'] = cli_pub_add[0]

  25.            now_user['cli_pub_port'] = cli_pub_add[1]

  26.            user_list.append(now_user)

  27.            toclient = 'info#%s login in successful , the info from server'%now_user['user_name']

  28.            sock.sendto(toclient,cli_pub_add)

  29. print'-'*100

  30. print"%s 已經登陸,公網IP:%s 端口:%d\n"%(now_user['user_name'],now_user['cli_pub_ip'],now_user['cli_pub_port'])

  31. print"如下是已經登陸的用戶列表"

  32. for one_user in user_list:

  33. 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'])

  34. elif cli_str['type'] == 'alive':

  35. pass

  36. elif cli_str['type'] == 'logout' :

  37. pass

  38. elif cli_str['type'] == 'getalluser' :

  39. print'-'*100

  40. for one_user in user_list :

  41.                        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'])

  42.                        sock.sendto(toclient,cli_pub_add)

  43. if __name__ == '__main__':

  44.    thread.start_new_thread(server_handle, ())

  45. print'服務器進程已啓動,等待客戶鏈接'

  46. whileTrue:

  47. for one_user in user_list:

  48.            toclient = 'keepconnect#111'

  49.            sock.sendto(toclient,(one_user['cli_pub_ip'],one_user['cli_pub_port']))  

  50.            time.sleep(1)


Client端代碼:


  
  
  
  
  1. #!/usr/bin/python

  2. #coding:utf-8

  3. import socket, SocketServer, threading, thread, time

  4. CLIENT_PORT = 4321

  5. SERVER_IP = "200.0.0.128"

  6. SERVER_PORT = 1234

  7. user_list = {}

  8. local_ip = socket.gethostbyname(socket.gethostname())

  9. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

  10. def server_handle():

  11. print'客戶端線程已經啓動 , 等待其它客戶端鏈接'

  12. whileTrue:

  13.        data, addr = sock.recvfrom(8192)

  14.        data_str = data.split('#')

  15.        data_type = data_str[0]

  16.        data_info = data_str[1]

  17. if data_type == 'info' :

  18. del data_str[0]

  19. print data_info      

  20. if data_type == 'getalluser' :

  21.            data_sp = data_info.split(' ')

  22.            user_name = data_sp[0].split(':')[1]

  23. del data_sp[0]

  24.            user_list[user_name] = {}

  25. for one_line in data_sp:

  26.                arg = one_line.split(':')

  27.                user_list[user_name][arg[0]] = arg[1]

  28. if data_type == 'echo' :

  29. print data_info

  30. if data_type == 'keepconnect':

  31.            messeg = 'type:alive'

  32.            sock.sendto(messeg, addr)

  33. if __name__ == '__main__':

  34.    thread.start_new_thread(server_handle, ())

  35.    time.sleep(0.1)

  36.    cmd = raw_input('輸入指令>>')

  37. whileTrue:

  38.        args = cmd.split(' ')

  39. if args[0] == 'login':

  40.            user_name = args[1]

  41.            local_uname = args[1]

  42.            address = "private_ip:%s private_port:%d" % (local_ip, CLIENT_PORT)

  43.            headder = "type:login\tuser_name:%s\tprivate_ip:%s\tprivate_port:%d" % (user_name,local_ip,CLIENT_PORT)

  44.            sock.sendto(headder, (SERVER_IP, SERVER_PORT))

  45. elif args[0] == 'getalluser':

  46.            headder = "type:getalluser\tuser_name:al"

  47.            sock.sendto(headder,(SERVER_IP,SERVER_PORT))

  48. print'獲取用戶列表中。。。'

  49.            time.sleep(1)

  50. for one_user in user_list:

  51. 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'])

  52. elif args[0] == 'connect':

  53.            user_name = args[1]

  54.            to_user_ip = user_list[user_name]['pub_ip']

  55.            to_user_port = int(user_list[user_name]['pub_port'])

  56. elif args[0] =='echo':

  57.            m = ' '.join(args[1:])

  58.            messeg = 'echo#from %s:%s'%(local_uname,m)

  59.            sock.sendto(messeg, (to_user_ip, to_user_port))

  60.        time.sleep(0.1)  

  61.        cmd = raw_input('輸入指令>>')

相關文章
相關標籤/搜索