这里基于stund的实现,来研究标准STUN协议,判断NatType的过程。
stund用于判断NatType的接口的用法
首先来看stund中用于判断NatType的接口的用法。这里主要来看stund中的STUN客户端client.cxx的实现。client.cxx是一个常规的C/C++ app,这个app的主要code如下:
可以看到这个app主要做了3件事情:
解析参数。主要从参数中获得STUN server的地址,及本地用于发送数据包所用的UDP端口号。
调用stunNatType()函数判断NatType。判断NatType的全部逻辑都在这个函数里。
将stunNatType()函数返回的NatType进行格式化并打印输出,以便于人的阅读。
接着来看stunNatType()函数的实现
stunNatType()函数的实现
stunNatType()函数的实现如下:
可以看到这个函数主要做了几件事:
打开了两个UDP socket。后续会通过这两个socket来进行数据包的发送,并最终根据这些数据包的响应数据包的情况来判断NatType。
向STUN server发送请求。调用stunSendTest()函数发送了5种不同类型的消息,各个消息之间的差异也仅仅在与stunSendTest()函数的testNum参数不同。这里我们也用testNum来区分不同的消息,我们称它们分别为类型1,类型2,类型3,类型10及类型11的消息。
其中类型10和类型11的消息依赖于类型1的消息的响应,但类型2和类型3的消息的发送则与类型1的消息的发送及响应相互独立,因而它们可以与类型1的消息并行的发送。接收发送的消息的响应。
从类型1的消息的响应中获得的东西比较多。类型10和类型11的消息要发送的目标地址,都来源于类型1的消息的响应。
类型10的消息发向类型1的消息的响应的changedAddress地址。这个地址是STUN server的副IP地址及端口号。
类型11的消息则发向类型1的消息的响应的testImappedAddr地址,这个地址是发送消息的地址的出口公网地址,向这个消息发送消息实际是向本节点在发送消息,这么做的实际目的是为了测试节点所连接的NAT是否支持消息的回传,或者说测试NAT是否是hairpin的。即如果这个类型11的消息通过NAT并最终被发送给本节点且本节点接收到了这个消息,则说明本节点所连接的NAT是hairpin的。
STUN终端会从类型10的消息的响应中获得相同的本地网络地址到另外的网络地址(IP地址与类型1的目标IP地址不同)的出口公网地址,并用这个地址与类型1的响应中携带的那个出口公网地址进行比较,以此来判断当前节点所连接的NAT是否是对称型的。
除了类型1和类型10之外,发送其它的消息主要就是看看是否能获得对应的响应。根据发送的这5种不同类型的消息的响应来判断当前节点所连接的NAT的类型并返回给调用者。
下面我们再用几张图来详细地说明,这些消息都发到了哪里,而响应又是从哪里返回回来的。
先说明一下,stund的STUN Server需要部署在一台具有双网卡且每个网卡都有一个自己公网IP地址的主机上。STUN Server的两个IP可以称为IPAddr1(primary IP)和IPAddr2(alt IP),两个端口可以称为Port1(primary port)和Port2(alt port),这两个端口默认分别为3478和3479。STUN Server会打开4个sockets,每个IP两个分别对应两个不同的端口。
首先是消息1:
消息1从客户端的第一个端口Port1发向STUN Server的IPAddr1:Port1,响应中则会携带客户端发送消息的端口的出口网络地址,及IPAddr2:Port2,以为后续发送消息10及消息11做准备。
消息2:
消息2从客户端的第二个端口,发向STUN Server的IPAddr1:Port1,这个消息请求STUN Server将响应从它的IPAddr2:Port1发送回来,也就是相对于接收数据包的网络地址而言切换一下IP地址的网络地址。
发送这个消息的目的是什么呢?这个消息的响应如果能接收到的话,说明当前节点连接的NAT的类型为全锥型的,说明NAT对于发向其内部的主机的数据包几乎没有限制。
这里为什么要从第二个端口发送消息呢?这主要是因为,类型10的消息会发向IPAddr2:Port1,这实际上会对消息2的响应的接收产生干扰。如果一个地址向IPAddr2:Port1发送了消息,即使当前节点连接的NAT的类型不是全锥型的,从IPAddr2:Port1发回来的消息也可能被接收到。
消息3:
消息3同样从客户端的第二个端口发出,且同样发向STUN Server的IPAddr1:Port1,但这个消息请求STUN Server将响应从它的IPAddr1:Port2发送回来,也就是相对于接收数据包的网络地址而言切换一下端口的网络地址。
在消息2的响应接收不到的情况下,如果消息3的响应可以接收到,说明NAT对传入给内部主机的包是限制IP而不限制端口的,也就是说当前节点连接的NAT的类型是IP限制型的。
消息4:
针对多主机部署的STUN Server优化
由上面的过程,不难看到,STUN Server的部署有一个比较大的限制,即要求部署的主机具有双网卡,这对于我们当前遍地云主机的环境而言,部署起来是不那么方便的。主要是对于类型2的消息,客户端请求STUN Server切换一下IP地址将消息发回来。
因而一种用于stund的STUN Server的优化设计应运而生,结构如下图:
这种设计主要是让STUN Server只绑定一个IP上的两个端口,同时在STUN之间建立一个通信信道,以便于类型2的消息能得到合适的处理。
针对多主机部署的STUN Server的优化当前实现的状况:
Github主页:https://github.com/hanpfei/stund
STUN消息的格式
具体可多主机部署的STUN Server要如何设计?这还要从STUN消息的具体格式说起。接着来看下STUN消息的具体格式。
首先是客户端发送的请求的格式。我们可以通过stunSendTest()函数的实现来对这个问题做一番了解:
|
|
从这里似乎也得不到太多STUN消息格式的具体信息,细节都被放在stunBuildReqSimple()和stunEncodeMessage()两个函数中了,接着来看这两个函数的实现:
由这些函数的实现,当不难理出来STUN请求消息的格式大体为:
整体来看,STUN请求消息分为两个部分,一部分是Header,另一部分是Attr的List。
而Header又包含消息的类型,消息不包含Header的长度,及一个128位16字节的id。在stund中,id的首个字节保存了消息的类型。STUN Server会原封不动的将客户端发过去的消息的id包含在响应中发回给客户端,在stund中,使用了id的首个字节用以区分发出去的不同类型的消息的响应。
Attr的List则是一系列的Attr。Attr的结构大体为,先是一个16位的AttrType,然后是16位的Attr值长度,接着便是Attr的值,而Attr的值所占字节数因Attr的不同而不同。对于判断NatType这个case而言,AttrList中只有一个Attr,及类型为ChangeRequest的Attr,它有一个32位4字节的值。这个Attr用于告诉STUN Server,响应应该从哪个网络地址发回来。
看完了STUN请求消息的格式之后,接着再来看STUN响应消息的格式。这个我们可以从stunServerProcessMsg()函数的实现来了解:
由这个函数的实现,我们不难看出STUN Server发回给客户端的响应的消息格式与请求的格式大体一样,但消息的具体内容有一些区别。消息的格式大体为:
这个消息里的内容要多一点。
了解了STUN客户端和STUN Server间交互的这些UDP数据包的格式之后,我们就可以确定可双主机部署的STUN Server间通信的消息的格式了。
仔细来看stunServerProcessMsg(),我们注意到,STUN server响应发送的目标地址,以及返回给客户端的它的出口公网地址也就是mappedAddress也没有限定只能是from地址,这些值也可以来源于请求消息。
借助于stund的这些良好设计,可以大大简化我们的可双主机部署的STUN server的设计与实现。STUN server间的消息格式可以为:
也就是说,当STUN Server收到类型2的消息时,构造一个格式如上图的消息,并将该消息转发给另为一个STUN Server。其中MappedAddress和ResponseAddress Attr的值都是消息的from地址,即客户端发送消息的端口的出口公网地址。
经过对stunServerProcessMsg()的一番改造,终于可以实现STUN Server的多主机部署,其改造后的实现为:
主要的改动即是在发现客户端请求改变IP地址发回响应时,构造如上图中的消息,并发给另一个STUN Server。从而,对于消息2,数据包的流转过程大体如下:
Done。