天天看點

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

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

   用python做了一個P2P的簡單實作,可以通過在服務端注冊公網IP和端口後,直接和其它用戶端進行通信。目前隻能在完全對稱NAT下成功實作

使用vmware模拟了内網、NAT和公網環境測試有效性,最終實作不通過公網伺服器的中轉,2個私網使用者進行直接聊天。目前實作的隻是聊天功能,在後續的應用用,也可以實作内網使用者的遠端控制,或者比如在家遠端連接配接公司内部伺服器進行遠端辦公等。

<a></a>

   首先,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裝置開啟的端口的規律,是以沒有去實作。

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屬于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,則不能雙向通信。

由于存在有4種NAT的類型,是以在方案設計中,必須要考慮到用哪種軟體或者裝置來模拟NAT環境。我在實作過程中嘗試過了很多選擇,包括Linux下的iptables,海蜘蛛的軟路由,使用CISCO的IOS模拟,window2003自帶的NAT服務,還有VMWARE自帶的NAT網絡環境。最終發現window2003和VMWARE是支援full cone nat的。

通過前人的試驗和我自己的驗證,iptables确實是貨真價實的Symmetric NAT。不過也有人通過改寫在Linux2.4核心下的iptables源碼将SymmetricNAT改為了Full Cone NAT,也可以通過編寫規則作弊的方式實作full nat cone,不過我最終沒有采取iptables。一是改寫源碼的方式比較繁瑣,需要修改很多個源碼檔案。而使用編寫規則作弊的方式也覺得有點自欺欺人了。

Window 2003自帶有路由和遠端通路服務,其中包含有NAT服務。經過測試,可以實作完全的Full Cone NAT。

我曾使用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' :

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

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'伺服器程序已啟動,等待客戶連接配接'

           toclient = 'keepconnect#111'

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

           time.sleep(1)

Client端代碼:

import socket, SocketServer, threading, thread, time

CLIENT_PORT = 4321

SERVER_IP = "200.0.0.128"

user_list = {}

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

print'用戶端線程已經啟動 , 等待其它用戶端連接配接'

       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)

   time.sleep(0.1)

   cmd = raw_input('輸入指令&gt;&gt;')

       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'擷取使用者清單中。。。'

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':

           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('輸入指令&gt;&gt;')

本文轉自lustlost 51CTO部落格,原文連結:http://blog.51cto.com/lustlost/1177494,如需轉載請自行聯系原作者