原来是本人的毕业设计。。。现在拿出来共享下。先申明,作者是lust,老师如果在网上搜到这段代码可不要说我的毕业设计是网上copy滴哈。。。
用python做了一个P2P的简单实现,可以通过在服务端注册公网IP和端口后,直接和其它客户端进行通信。目前只能在完全对称NAT下成功实现
使用vmware模拟了内网、NAT和公网环境测试有效性,最终实现不通过公网服务器的中转,2个私网用户进行直接聊天。目前实现的只是聊天功能,在后续的应用用,也可以实现内网用户的远程控制,或者比如在家远程连接公司内部服务器进行远程办公等。
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('输入指令>>')
本文转自lustlost 51CTO博客,原文链接:http://blog.51cto.com/lustlost/1177494,如需转载请自行联系原作者