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端代码:


  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(‘输入指令>>’)

Python实现简单的udp打洞(P2P)的更多相关文章

  1. UDP打洞、P2P组网方式研究

    catalogue . NAT概念 . P2P概念 . UDP打洞 . P2P DEMO . ZeroNet P2P 1. NAT概念 在STUN协议中,根据内部终端的地址(LocalIP:Local ...

  2. p2p的UDP打洞原理

    >>>>>>>>>>>>>>>>>>>>>>>>> ...

  3. C# p2p UDP穿越NAT,UDP打洞源码

    思路如下(参照源代码): 1. frmServer启动两个网络侦听,主连接侦听,协助打洞的侦听. 2. frmClientA和frmClientB分别与frmServer的主连接保持联系. 3. 当f ...

  4. NAT穿透(UDP打洞)

    1.NAT(Network Address Translator)介绍 NAT有两大类,基本NAT和NAPT. 1.1.基本NAT 静态NAT:一个公网IP对应一个内部IP,一对一转换 动态NAT:N ...

  5. UDP 打洞 原理解释

    终于找到了一份满意的UDP打洞原理解释,附上正文,自己整理了一下源码 3.3. UDP hole punching UDP打洞技术 The third technique, and the one o ...

  6. udp打洞( NAT traversal )的方法介绍

    http://www.cnblogs.com/whyandinside/archive/2010/12/08/1900492.html http://www.gzsec.com/oldversion/ ...

  7. Python基础教程之udp和tcp协议介绍

    Python基础教程之udp和tcp协议介绍 UDP介绍 UDP --- 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议.UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但 ...

  8. Udp打洞原理和源代码。

    所谓udp打洞就是指客户端A通过udp协议向服务器发送数据包,服务器收到后,获取数据包,并且 可获取客户端A地址和端口号.同样在客户端B发送给服务器udp数据包后,服务器同样在收到B发送过来 的数据包 ...

  9. UDP打洞原理及代码

    来源:http://www.fenbi360.net/Content.aspx?id=1021&t=jc UDP"打洞"原理 1.       NAT分类 根据Stun协议 ...

随机推荐

  1. 轻量级RPC

    ①自定义一个协议接口继承VersionedProtocol ②自定义协议类实现上面的接口,完善功能需求 ③服务端 ④客户端 二:模拟一个namenode

  2. 2017-5-5/PHP实现负载均衡的加权轮询

    1. 负载均衡算法有哪些? 轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载. 随机法:通过系统的随机算法,根据后端服务器的列表 ...

  3. 2015-09-22 css2

    6.块元素和行内元素 1. 块元素特点:默认显示在父标签的左上角 块级元素默认占满一行(占满整个文档流) 常见的块元素:p,h1--h6,ul li, ol li,div,hr,table. 2.行内 ...

  4. daal4py 随机森林模型训练mnist并保存模型给C++ daal predict使用

    # daal4py Decision Forest Classification Training example Serialization import daal4py as d4p import ...

  5. oracle配置访问白名单教程

    出于提高数据安全性等目地,我们可能想要对oracle的访问进行限制,允许一些IP连接数据库或拒绝一些IP访问数据库. 当然使用iptables也能达到限制的目地,但是从监听端口变更限制仍可生效.只针对 ...

  6. npm run build 打包后,如何运行在本地查看效果(Apache服务)

    目前,使用vue-cli脚手架写了一个前端项目,之前一直是使用npm run dev 在8080端口上进行本地调试.项目已经进行一半了,今天有时间突然想使用npm run build进行上线打包,试试 ...

  7. 窗体应用程序防腾讯QQ源码

    窗体应用程序防腾讯QQ源码 using System; using System.Collections.Generic; using System.ComponentModel; using Sys ...

  8. error TS2304: Cannot find name 'Promise' && TS2307: Cannot find module '**'

    error TS2304: Cannot find name 'Promise' 解决方法:在编译选项中加入"target":"es6" { "ver ...

  9. 1-4Controller之Middleware

    1.注册中间件: 新建中间件文件:Activity <?php namespace App\Http\Middleware; use Closure; class Activity{ publi ...

  10. css3 居中(推荐弹性盒模型方式)

    参考  http://www.zhihu.com/question/20774514 http://caibaojian.com/demo/flexbox/align-items.html 例子:ht ...