Java UDP打洞示例 - 通过防火墙连接

36

假设我有两台计算机。

它们通过 ice4j 知道彼此的公网IP和私有IP。

一个客户端正在监听,另一个客户端正在发送一些字符串。

我希望通过UPD打洞来实现这个过程:

Let A be the client requesting the connection

Let B be the client that is responding to the request

Let S be the ice4j STUN server that they contact to initiate the connection
--
A sends a connection request to S

S responds with B's IP and port info, and sends A's IP and port info to B

A sends a UDP packet to B, which B's router firewall drops but it still
punches a hole in A's own firewall where B can connect

B sends a UDP packet to A, that both punches a hole in their own firewall,
and reaches A through the hole that they punched in their own firewall

A and B can now communicate through their established connection without 
the help of S

是否有人能够提供关于如何通过对称NAT进行洞穴穿透的伪代码示例?假设有一个服务器S将帮助猜测端口号并在客户端A和B之间建立连接。

如果您考虑到双重NAT,那就太好了。

注意:

您可以使用STUN发现IP和端口,但必须编写自己的代码通过keepalive技术将IP:Port发送到服务器。一旦一个客户端通过服务器上的唯一ID识别另一个客户端,它将提供其他客户端的IP:端口信息以UDP打洞所需的数据,以便发送和接收。

小更新:

有一个Java库正在崛起,请查看:
https://github.com/htwg/UCE#readme


1
设置本地端口并不能保证在NAT转换为公共IP后仍然使用相同的端口,因此以通用方式解决这个问题并不像看起来那么容易。 - Joachim Isaksson
请展示一些源代码...你尝试过什么?有什么不起作用的地方吗? - Yahia
从我的角度来看,@MatBanik的问题非常广泛...请仔细阅读您提供的链接中的代码,并提出具体问题-这将得到更好的答案,我认为... - Yahia
@MatBanik,也许我发布的链接可以帮助你的工作 :-) - Yahia
4个回答

53
这个例子是使用C#编写的,而不是Java,但NAT穿透的概念与语言无关。请查看Michael Lidgren的网络库,其中包含了NAT穿透功能。
链接: http://code.google.com/p/lidgren-network-gen3/ 处理NAT穿透的特定C#文件: http://code.google.com/p/lidgren-network-gen3/source/browse/trunk/Lidgren.Network/NetNatIntroduction.cs 您发布的过程是正确的。对于NAT设备的4种常规类型中的3种,它将起作用(我说常规是因为NAT行为并没有真正标准化):完全锥形NAT、受限锥形NAT和端口受限制的锥形NAT。NAT遍历无法与对称NAT一起使用,对称NAT主要用于企业网络以增强安全性。如果一方使用对称NAT,而另一方不使用,则仍然可以穿越NAT,但需要更多的猜测。对称NAT到对称NAT的穿越非常困难 - 您可以在此处阅读有关此问题的论文
但是,实际上,您描述的过程完全有效。我已经为我的远程屏幕共享程序(也是C#)实现了它。只需确保已禁用Windows防火墙(如果您正在使用Windows)和第三方防火墙。但是,我可以高兴地确认它会起作用。 澄清NAT遍历的过程 我写这篇更新是为了为您和未来的读者澄清NAT穿透的过程。希望这可以清晰地总结历史和过程。
一些参考来源:http://think-like-a-computer.com/2011/09/16/types-of-nat/,以及http://en.wikipedia.org/wiki/Network_address_translationhttp://en.wikipedia.org/wiki/IPv4http://en.wikipedia.org/wiki/IPv4_address_exhaustion
IPv4地址的容量大约能够唯一命名43亿台计算机,已经用完。聪明的人们预见到了这个问题,并发明了路由器等设备来解决IPv4地址耗尽的问题,通过分配一个共享IP地址给连接在一起的计算机网络。

有局域网IP和广域网IP。局域网IP是本地区域网络中唯一标识计算机的IP,例如连接到家用路由器的台式机、笔记本电脑、打印机和智能手机。广域网IP则是在广域网络中唯一标识本地区域网络以外计算机的IP——通常指互联网。因此,这些路由器会分配一个组的计算机1个WAN IP。每台计算机仍然有自己的LAN IP。当你在命令提示符中键入ipconfig并获得IPv4地址 . . . . . . . . 192.168.1.101时,你看到的是LAN IP。当你连接到cmyip.com并获得128.120.196.204时,你看到的是WAN IP。

就像无线电频谱被收购一样,整个IP范围也被机构和组织购买和保留了,还有端口号。总之,我们没有多余的IPv4地址可供使用。

这与NAT穿透有什么关系呢?自路由器问世以来,直接连接(端对端连接)就有些不可能了,除非进行一些黑科技。如果你有两台计算机(计算机A和计算机B),它们都共享128.120.196.204的WAN IP,那么连接将会到哪台计算机呢?我指的是一个外部计算机(比如google.com)发起128.120.196.204的连接。答案是:没有人知道,路由器也不知道,这就是为什么路由器会丢弃连接。如果计算机A 发起google.com的连接,那就是另一回事了。路由器会记住计算机A的LAN IP 192.168.1.10174.125.227.64(即google.com)发起连接的情况。当计算机A的请求数据包离开路由器时,路由器实际上会重新编写LAN IP 192.168.1.101为路由器的WAN IP 128.120.196.204。因此,当google.com收到计算机A的请求数据包时,它看到的是路由器重新编写的发送方IP,而不是计算机A的LAN IP(google.com将128.120.196.204视为要回复的IP)。当google.com最终回复时,数据包到达路由器,路由器记住了(它有一个状态表)它正在等待google.com的回复,并适当地将数据包转发给计算机A。
换句话说,当您启动连接时,路由器没有问题 - 路由器会记住将回复数据包转发回您的计算机(通过上述整个过程)。但是,当外部服务器向您发起连接时,路由器无法知道该连接是针对哪台计算机的,因为计算机A和计算机B都共享128.120.196.204的WAN IP地址...除非有明确的规则指示路由器将最初前往目标端口X的所有数据包转发到计算机A,目标端口Y。这就是所谓的“端口转发”。不幸的是,如果您考虑使用端口转发进行网络应用程序,这是不切实际的,因为您的用户可能不知道如何启用它,并且如果他们认为这是安全风险,他们可能不愿意启用它。UPnP只是指允许您“以编程方式启用端口转发”的技术。不幸的是,如果您考虑使用UPnP来进行网络应用程序的端口转发,这也是不切实际的,因为UPnP并不总是可用的,而且当它可用时,可能默认未开启。
那么解决方案是什么呢?解决方案要么是将您的所有流量代理到您自己的计算机上(您已经仔细预先配置为全球可达),要么想出一种方法来打破这个系统。第一个解决方案被称为TURN,以提供一组带有可用带宽的服务器的价格为代价,神奇地解决了所有连接问题。第二个解决方案称为NAT穿透,这是我们接下来要探讨的内容。
早些时候,我描述了外部服务器(比如google.com)发起与128.120.196.204的连接过程。我说,如果路由器没有特定规则来理解将google的连接请求转发到哪台计算机,路由器将会简单地中断连接。这是一个概括性的情景,并不准确,因为有不同类型的NAT。 (注:路由器是您可以放在地板上的实际物理设备。NAT(网络地址转换)是编程到路由器中的软件过程,可帮助节省IPv4地址,就像树木一样)。因此,根据路由器使用的NAT,连接场景会有所不同。 路由器甚至可能结合NAT过程。
有四种标准化的NAT类型:完全锥形NAT、受限制锥形NAT、端口受限制锥形NAT和对称NAT。除了这些类型,还可能存在具有非标准化行为的其他类型的NAT,但这种情况较少发生。
注意:我对NAT并不太熟悉……似乎有很多关于路由器的看法,而且互联网上关于这个主题的信息非常分散。维基百科说将NAT按完全锥形、受限制和端口受限制锥形分类已经有点过时了?还有一些叫做静态和动态NAT的东西……只是一堆无法协调的各种概念。尽管如此,以下模型适用于我的应用程序。您可以通过阅读下面和上面以及本帖中的链接了解更多关于NAT的信息。我不能再发表更多关于它们的内容,因为我对它们的了解不是很多。
希望一些网络大师能纠正/添加输入,以便我们都能更多地了解这个神秘的过程。 回答您的问题,关于收集每个客户端的外部IP和端口:

所有UDP数据包的首部结构相同, 具有一个源IP和一个源端口。 UDP数据包头部不包含"内部"源IP和"外部"源IP。UDP数据包头部只包含一个源IP。如果您想获得"内部"和"外部"源IP,您需要将内部源IP作为负载的一部分实际发送。但是,听起来您并不需要内部源IP和端口。根据您的问题所述,似乎您只需要外部IP和端口。这意味着您的解决方案只需像字段一样读取数据包的源IP和端口。

以下是两种情况(它们并没有解释其他任何内容):

局域网通信

计算机A的局域网IP为192.168.1.101。计算机B的局域网IP为192.168.1.102。计算机A从端口3000向计算机B的端口6000发送数据包。UDP数据包的源IP将是192.168.1.101,这将是唯一的IP地址。“外部”在这里没有上下文,因为该网络纯粹是局域网。在此示例中,广域网(如互联网)不存在。关于端口,由于我不确定NAT,所以我不确定数据包上所写的端口是否为3000。NAT设备可能会将数据包的端口从3000重写为类似49826的随机端口。无论哪种方式,您都应该使用数据包所写的端口来回复 - 这就是您应该用来回复的端口。因此,在这个局域网通信的示例中,您只需要发送一个IP地址 - 局域网IP地址,因为这是唯一重要的。您不必担心端口 - 路由器会为您处理。当您接收到数据包时,只需从数据包中读取唯一的IP地址和端口即可。
WAN通信
计算机A的局域网IP地址是192.168.1.101,计算机B的局域网IP地址是192.168.1.102。计算机A和计算机B将共享一个WAN IP地址128.120.196.204。服务器S是一台服务器,可以在全球范围内访问,例如Amazon EC2服务器,其WAN IP地址为1.1.1.1。服务器S可能有一个局域网IP地址,但这与本题无关。计算机B也与本题无关。
计算机A从端口3000向服务器S发送一个数据包。在路由器外传输时,数据包的源局域网IP地址从计算机A被重写为路由器的广域网IP地址。路由器还将源端口从300重写为32981。服务器S在外部IP和端口方面看到了什么?服务器S看到的是128.120.196.204作为IP地址,而不是192.168.1.101,并且服务器S看到的是32981作为端口,而不是3000。虽然这些不是计算机A用于发送数据包的原始IP和端口,但这些是正确的IP和端口以进行回复。当您接收数据包时,您只能知道WAN IP和重写端口。如果这就是您想要的(您正在寻求外部 IP和端口),那么您已经准备好了。否则,如果您还想要发送者的内部IP,则需要将其作为普通数据分离从标题中传输。 代码: 如上所述(下面回答您的问题关于收集外部IP),要收集每个客户端的外部IP和端口,您只需从数据包中读取它们。每个发送的数据报总是具有发送者的源IP和源端口;您甚至不需要一个花哨的自定义协议,因为这两个字段总是包含在内 - 每个单独的UDP数据包必须通过定义具有这两个字段。
// Java language
// Buffer for receiving incoming data
byte[] inboundDatagramBuffer = new byte[1024];
DatagramPacket inboundDatagram = new DatagramPacket(inboundDatagramBuffer, inboundDatagramBuffer.length);
// Source IP address
InetAddress sourceAddress = inboundDatagram.getAddress();
// Source port
int sourcePort = inboundDatagram.getPort();
// Actually receive the datagram
socket.receive(inboundDatagram);

因为getAddress()getPort()可以返回目标端口或源端口,这取决于您设置的内容,在客户端(发送)机器上,调用setAddress()setPort()到服务器(接收)机器,而在服务器(接收)机器上,调用setAddress()setPort()回到客户端(发送)机器。必须有一种方法在receive()中实现此操作。如果这是您实际遇到的问题(getAddress()getPort()未返回您期望的源IP和端口),请详细说明。这假定服务器是一个“标准”的UDP服务器(它不是STUN服务器)。 进一步更新: 我看到了你关于“如何使用STUN获取一个客户端的IP和端口并将其提供给另一个客户端”的更新?STUN服务器不是用来交换端点或执行NAT穿透的。STUN服务器的设计目的是告诉你公共IP、公共端口和NAT设备类型(无论是完全锥形NAT、受限锥形NAT还是端口受限锥形NAT)。我会称负责交换端点和执行实际NAT穿透的中间人服务器为“介绍者”。在我的个人项目中,我实际上不需要使用STUN来执行NAT穿透。我的“介绍者”(介绍客户端A和B的中间人服务器)是一个标准服务器,监听UDP数据报。当客户端A和B都向介绍者注册时,介绍者读取它们的公共IP和端口以及私有IP(如果它们在局域网上)。公共IP从数据报头中读取,就像所有标准UDP数据报一样。私有IP作为数据报有效载荷的一部分写入,介绍者只需将其作为有效载荷的一部分读取即可。因此,关于STUN的实用性,您不需要依赖STUN来获取每个客户端的公共IP和公共端口-任何连接的套接字都可以告诉您这一点。我会说STUN仅在确定客户端所处的NAT设备类型时有用,以便您知道是否执行NAT穿透(如果NAT设备类型是完全锥形、受限或端口受限),或者执行全面的TURN流量代理(如果NAT设备类型是对称的)。
请详细说明您的障碍:如果您想获得有关设计应用程序消息协议的最佳实践建议,并获得有关按有序和系统化方式读取接收到的消息字段的建议(基于您在下面发布的评论),您能否分享您当前的方法?

1
这个回答中存在一些完全错误和误解:i)TURN是NAT穿透的一种类型,ii)NAT穿透不是TURN的“替代品”,iii)锥形分类已经被Saikat Guha的工作(http://www.mpi-sws.org/~francis/imc05-tcpnat.pdf,请参见表6,甚至有一个RFC,维基百科在这个问题上完全过时了)所淘汰。 - Jérôme Verstrynge
2
iv) STUN有几个版本,v) 为了证明STUN在NAT穿越中的冗余性而提出的论点只是揭示了作者在这个问题上的即兴发挥和对此问题的不充分理解。具体来说,STUN服务器执行端口预测并在可能时帮助建立直接的P2P TCP连接的有用性,以及当不可能时需要退回到类似TURN的解决方案。这个答案包含了很多基于荒谬假设和过时知识的推测理论。它真的没有帮助理解P2P和NAT穿越。 - Jérôme Verstrynge
2
关于i),作者混淆了NAT穿透和打洞。 - Jérôme Verstrynge
2
抱歉,我不是故意发布错误信息的。我在谷歌上搜索的NAT穿透/UDP打洞文章对我的项目有用,所以我认为这就是它的工作原理。 - Jason
1
多么棒且富有洞见的回答。谢谢你。 - Martin Tuskevicius

10

6
STUN基本上是这样工作的:位于防火墙后的客户端连接到一个位于防火墙外的STUN服务器。 STUN服务器检查从客户端接收到的数据包,并向客户端发送一个响应,其中包含客户端的IP和端口,就像它们在STUN服务器中显示的那样。
这就是防火墙后的客户端发现自己的外部IP和端口的方式。据我所知,STUN服务器通常不会将地址信息从一个客户端传递给另一个客户端。
通常情况下,当防火墙已经开放信令流量时,STUN用于设置通过防火墙的媒体流 - 例如在VoIP中:客户端联系STUN服务器以发现其UDP流量的外部IP和端口,然后将其信令请求(SIP INVITE或其他)发送到另一个已知的开放端口上的另一个客户端 - 在有效负载(SDP或其他)中包括其外部UDP地址信息。因此,通常一个客户端需要通过开放端口可达,以进行点对点通信。

6

您的问题与Java无关。如果您知道如何打开UDP连接,那就足够了。阅读以下链接的内容。不要被标题吓到,它也涵盖了UDP。其余部分只是Java编码。

P.S.:在您的场景中,缺少一个步骤。A和B都必须与S建立开放连接,因为S需要告诉B A正在尝试联系它。如果B没有与S建立开放连接,则A和B无法开始相互通信。

更新

Jason所做的答案包含有关NAT穿透的错误和猜测。应该阅读Saikat Guha (mpi-sws.org/~francis/imc05-tcpnat.pdf)所做的工作,以真正理解这个问题。 Wikipedia的锥形分类完全过时且具有误导性。


这些来自Saikat Guha论文的语句对于理解STUN非常有帮助。在STUN中,Alice向Bob发送一个UDP数据包。尽管Bob的NAT会删除此数据包,但它会使Alice的NAT创建本地状态,从而使Bob的响应定向到Alice。然后,Bob向Alice发送一个UDP数据包。Alice的NAT将其视为第一个数据包的流程的一部分并将其路由通过,而Bob的NAT则将其视为连接初始化并创建本地状态以路由Alice的响应。这种方法被Skype所使用,这是一种流行的VoIP应用程序。 - Kalyan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接