P2P连接
前面我们已经知道了 WebRTC
通信时会按照内网、P2P、relay
这样的次序尝试连接,但由于大部分通信的双方都在不同的网段,因此对于 WebRTC
来说,P2P 和 relay
才是它的主要应用场景,接下来重点对这两种场景进行介绍。
首先看一下 P2P 场景。实际上,这里的 P2P 指的就是如何进行 NAT 穿越。NAT 在真实的网络环境中随处可见,它的出现主要出于两个目的。一是为了解决 IPv4
地址不够用的问题。当时 IPv6
短期内还无法替换 IPv4
,而 IPv4
的地址又特别紧缺,所以人们想到让多台主机共用一个公网 IP
地址,大大减缓了 IPv4
地址不够用的问题。二是为了解决安全问题。使用 NAT
后,主机隐藏在内网,这样黑客就很难访问到内网主机,从而达到保护内网主机的目的。图 6.2 中给出了 NAT
网络示意图。

不过凡事有利有弊,NAT
的引入确实带来了好处,但同时也带来了麻烦。如果没有 NAT
,两台主机之间的连接会非常简单,实现像微信、QQ
这类产品的技术难度就会大大降低,而现在要想实现这类产品就必须考虑如何穿越 NAT
了。
想要进行 NAT
穿越,首先要清楚 NAT
穿越的原理。其实,NAT
就是一种地址映射技术,它在内网地址与外网地址之间建立了映射关系,如图 6.3 所示。当内网主机向外网主机发送信息时,数据在经过 NAT
层时,NAT
会将数据包头中的源 IP
地址和源端口号替换为映射后的外网 IP
地址和外网端口。相反,当接收数据时,NAT
收到数据后会将目标地址映射为内网的 IP
地址和端口再转给内网主机。

随着时间的推移,NAT
的规则越来越复杂,尤其是针对各种安全的需要,这就导致 NAT
越来越难以穿越。不过,难以穿越并不代表不能穿越,人们也在不断总结穿越 NAT
的经验,其中 RFC3489 和 RFC5389 就是最重要的两份 NAT
穿越的协议文档。在 RFC3489 协议中,将 NAT
分成 4 种类型,即完全锥型、IP限制锥型、端口限制锥型以及对称型。在这 4 种类型中,越往后的 NAT
类型穿越难度越大。
完全锥型NAT
完全锥型 NAT
的特点:一旦打洞成功,所有知道该洞的主机都可以通过它与内网主机进行通信。
如图 6.4 所示,当 host
主机通过 NAT
访问外网主机 B 时,就会在 NAT
上打个洞。如果主机 B 将该洞的信息分享给主机 A 和 C,那么知道这个洞的主机 A 和 C 都可以通过该洞给内网的 host
主机发送信息。

实际上,这里所谓的 “洞” 就是在 NAT
上建立了一个内外网的映射表。你可以将这个映射表简单地理解为一个 4 元组,包括内网IP、内网端口、映射的外网 IP
以及映射的外网端口。其格式如下:
{
内网IP,
内网端口,
映射的外网IP,
映射的外网端口
}
在 NAT
上有了这张映射表,所有发向这个洞的数据都会被 NAT
中转到内网的 host
主机。而在 host
主机上,侦听其内网端口的应用程序可以收到所有发向它的数据,是不是很神奇呢?
需要注意的是,大多数情况下 NAT
穿越使用的是 UDP
,这是因为 UDP
是无连接协议的,打洞会更加方便。当然,也可以使用 TCP
打洞。对于 TCP
打洞的细节这里就不介绍了,如果读者有感兴趣的话可以自行查阅相关资料。
IP限制锥型NAT
IP
限制锥型 NAT
要比完全锥型 NAT
严格得多。IP
限制锥型 NAT
的主要特点:NAT
打洞成功后,只有与之打洞成功的外网主机才能通过该洞与内网主机通信,而其他外网主机即使知道洞口也不能与之通信。
如图 6.5 所示,host
主机访问主机B时,在 NAT
上打了一个洞。此时,只有主机 B 才能通过该洞向内网 host
主机发送信息,而其他外网主机(如 A 与 C)不能再像完全锥型 NAT
一样通过该洞与内网 host
主机通信了。但需要注意的是,主机 B 上不同的端口(如 p1、p2 等)是可以向 host
主机发送消息的。

之所以会如此,是因为 NAT
对穿越洞口的 IP
地址做了限制,只有登记过的外网 IP
地址才可以通过 NAT
。换句话说,只有 host
主机访问过的外网主机才能穿越这个洞。
IP
限制锥型 NAT
是如何检测数据包是否合法的呢?当外网主机通过 IP
限制锥型 NAT
向内网主机发送信息时,NAT
会检测数据包头中的源 IP
地址是否在 NAT
映射表中有记录。如果有记录,说明是合法数据,可以进行数据转发;如果没有记录,说明是非法数据,NAT
会将该数据包直接丢弃。
IP
限制锥型 NAT
的打洞映射表是一个 5 元组,包括内网 IP、内网端口、映射的外网IP、映射的外网端口以及被访问主机的 IP 列表。其格式如下:
{
内网IP,
内网端口,
映射的外网IP,
映射的外网端口,
[ 被访问主机的IP , … ]
}
通过上面的描述我们知道,IP
限制锥型 NAT
比完全锥型 NAT
对数据包的控制更严格,只有通过 IP
检测的数据包才能穿越 IP
限制锥型 NAT
。此外,由于 IP
限制锥型 NAT
只对 IP
做检测,因此只要 IP
检测通过了,使用哪个端口就无所谓了。
端口限制锥型NAT
端口限制锥型 NAT
比 IP
限制锥型更加严格。端口限制锥型 NAT
的主要特点:除了像 IP
限制锥型 NAT
一样需要对 IP
地址进行检测外,还需要对端口进行检测。
如图 6.6 所示,host
主机访问主机B时在 NAT
上打了一个洞,此时外网主机 A 和 C 是访问不了内网 host
主机的,这与 IP
限制锥型 NAT
是一样的。此外,如果 host
主机访问的是主机B的 p1
端口,那么只有主机 B 的 p1
端口发送的消息才能穿越 NAT
,而主机 B 的 p2
端口已无法再通过 NAT
了。

所以,虽然端口限制型 NAT
的映射表也是一个 5 元组,但它与 IP
限制锥型 NAT
的映射表还是有重要区别的,端口限制锥型 NAT
的映射表包括内网IP、内网端口、映射的外网IP、映射的外网端口以及被访问主机的 IP
和被访问主机的端口的组合。其格式如下:
{
内网IP,
内网端口,
映射的外网IP,
映射的外网端口,
[
{ 被访问主机的IP , 被访问主机的端口 },
…
]
}
从上面的格式中可以发现,与 IP 限制锥型 NAT 的 5 元组相比,端口限制锥型 NAT 的 5 元组的最后一个元组变成了要访问的主机的 IP 地址和端口的组合。对数据包的检测也从原来的只检测源 IP 地址变成了检测源 IP 地址和源端口。
通过前面的描述,我们知道从完全锥型 NAT 到端口限制型 NAT 一级比一级严格。不过端口限制型 NAT 还不是最严格的,最严格的是接下来要讲的对称型 NAT。
对称型NAT
对称型 NAT 是 4 种 NAT 类型中对数据包检测最严格的。对称型 NAT 的特点:内网主机每次访问不同的外网主机时,都会生成一个新洞,而不像前面 3 种 NAT 类型使用的是同一个洞。
如图6.7 所示,在对称型 NAT 中,host 主机访问主机 B 时在 NAT 上打了一个洞,此时只有主机 B 上相应端口发送的数据才能穿越该洞,这一点与端口限制型 NAT 是一致的。它与端口限制型 NAT 最大的不同是当 host 主机访问外网主机 A 时,它与主机 A 之间会新建一个洞,而不是复用访问主机 B 时的洞。
也就是说,对于对称型 NAT 来说,访问不同的主机会产生不同的新洞,这给我们进行 NAT 穿越造成了极大的麻烦,对于这一点,后面我们还会做更详细的讨论。
对称型 NAT 的映射表是一个 6 元组,其映射的外网 IP 地址和外网端口会随着访问目的主机的不同而变化。如访问主机 B 时映射的外网 IP 地址和外网端口与访问主机 A 或主机 C 时映射的外网 IP 地址和外网端口是不一样的。
对称型 NAT 每次访问不同外网主机都生成新洞的这种特性,导致对称型 NAT 碰到对称型 NAT 或者对称型 NAT 遇到端口限制型 NAT 时,双方打洞的成功率非常低,即使可以互通,成本也非常高。WebRTC 遇到上面这两种情况时,直接放弃打洞的尝试。

{
内网IP,
内网端口,
// 不仅访问地址变化了, 映射IP 也要发生变化
映射的外网IP,
// 不仅访问端口变化了, 映射端口也要发生变化
映射的外网端口,
被访问主机的IP,
被访问主机的端口
}
NAT类型检测
通过前面的讲解,我们已经知道如何判断 NAT 的类型了。但对于内网主机来说,它是如何知道自己是哪种 NAT 类型的呢?实际上,RFC3489 协议中已经给出了标准的检测流程,图6.8
就是该标准检测流程的流程图。其中标注为➊的框中是几个重要的检测点,通过这几个检测点,主机就可以很容易地检测出自己属于哪种 NAT 类型。
需要注意的是,内网主机进行 NAT 类型检测时,需要用到两台 STUN 服务器,每台 STUN 服务器又需要两块网卡,每块网卡都需要配置公网 IP 地址,只有这样,环境才能符合图6.8 中流程的需求。下面详细分析一下这个流程图。

在图 6.8 的中间有一条虚线,将整张图分为左右两部分。其中左半部分是用来判断内网主机是否有 NAT 防护的,而右半部分则是用来探测主机在哪种 NAT 类型之后。
首先看一下内网主机是否有 NAT 防护这部分,如图 6.9 所示。内网主机首先向1号服务器的某个 IP 地址和端口发送一个 STUN 请求,服务器收到请求后,使用同样的 IP 地址和端口向主机返回一个 STUN 响应消息。
当内网主机向1号服务器发送 STUN 请求后,会启动一个定时器,如果在规定的时间内无法收到服务器的响应消息,那么说明主机与服务器之间的 UDP 不通,检测流程到此结束。相反,如果在规定的时间内收到了服务器的响应包,则需要进一步判断。
实际上,在服务端的返回消息里记录着主机的公网 IP 地址,所以当主机收到服务端响应后,它要判断带回的公网 IP 地址是否与自己本地的 IP 地址一致,以此判断主机是否在 NAT 之后。
如果本地 IP 地址与带回的公网 IP 地址一致,则说明此主机在公网上,没有 NAT 防护;如果不一致,则说明主机在 NAT 保护之下,需要对其进行 NAT 类型检测。对于 NAT 类型检测的流程,这里先不讲解,留待后面再做详细介绍。

当判断出主机在公网没有被 NAT 保护时,接下来要判断主机是否在防火墙之后,因为防火墙也会限制主机的数据发送与接收。此时,主机会再次向1号服务器发送请求,1号服务器收到主机的第二次请求后,使用第二块网卡给主机返回响应消息。
如果主机能收到服务器返回的消息,说明它是一台没有任何防护的公网主机,可以向任何主机发送数据,也可以从任何主机接收数据;否则说明有对称性防火墙保护着它,将其归类为对称型 NAT 即可。
图 6.10 是对上述处理流程的一个总结,判断主机是否有 NAT 防护一共需要四步:前两步是为了判定主机是否有 NAT 保护,后两步是为了判断主机是否有防火墙保护。通过这张图可以让你对整个过程有更加清晰的了解。至此,我们就将图 6.8 中 NAT 类型检测的左侧部分介绍完了。

接下来我们看一下图 6.8 中 NAT 类型检测的右半部分,如图 6.11 所示。这部分才是 NAT 类型检测的主流程。其最多经过三次检测就可以将主机所在的 NAT 类型确定下来。
当主机发现它是在 NAT 之后,则开始 NAT 类型检测。它首先向1号服务器(server(#1))发 STUN 请求,1号服务器收到请求后使用第二块网卡给主机返回响应消息。这样做的目的是判断NAT的类型是否为完全锥型,因为完全锥型NAT的特点是只要打洞成功就可以接收任何主机、任意端口发来的数据。
此外,主机向服务器发送请求后,会启动一个定时器。如果在规定的时间内收到了服务器的响应信息,则说明主机是在完全锥型 NAT 之下;否则,如果在规定的时间内收不到服务端的响应消息,那还需要对主机的NAT类型做下面的检测。
主机向2号服务器(server(#2))发送 STUN 请求,其目的是通过判断1号服务器得到的主机外网 IP 地址与通过2号服务器得到的主机的外网 IP 地址是否一致来判定其 NAT 类型。2号服务器收到消息后,将主机的外网 IP 地址和端口作为响应消息的内容返回给主机。
主机收到消息后,将响应消息中的外网 IP 地址与从1号服务器返回的外网 IP 地址做比较,如果不一致,说明这台主机是在对称型 NAT 之下。相反,如果 IP 地址一样,那么主机还需要再次发送请求做进一步的分析。

此时,主机再次转向1号服务器发送请求。1号服务器收到消息后,使用相同的 IP 地址(即使用接收消息的网卡)和不同的端口向主机返回响应消息。如果主机可以收到响应消息,说明主机是处于 IP 限制型 NAT 之下,否则说明主机是在端口限制型 NAT 之下。
至此,主机所处的 NAT 类型就被准确地判断出来了。有了主机的 NAT 类型,就可以很容易判断出两个主机之间到底能否成功地进行 NAT 穿越。
如何进行NAT穿越
主机有了判断自己在哪种 NAT 之下的能力后,就可以判断出与各主机之间是否可以进行 NAT 穿越了。表 6.2 指明了各种 NAT 之间是否可以穿越成功的结果,主机进行 NAT 穿越时也是依据这张表来实际操作的。
从这张表中可以看到,完全锥型 NAT 以及 IP 限制型 NAT 可以与任何其他类型的 NAT 互通,而端口限制型 NAT 与对称型 NAT、对称型 NAT 与对称型 NAT 之间互通的成本太高,所以遇到这两种情况时就不必再尝试 NAT 穿越了。

那么表 6.2 中的结果是如何推断出来的呢?这里举一个 IP 限制型 NAT 与对称型 NAT 之间穿越的例子,你就清楚整个推断过程了。根据这个推断过程可以将其他情况下 NAT 穿越的结果也推导出来。
如图 6.12 所示,IP 限制型 NAT 与对称型 NAT 该如何互通呢?根据本章前面介绍的内容,你应该知道 IP 限制型 NAT 只对 IP 地址进行限制,而对称型 NAT 则对每个不同的外网主机都会产生一个新洞。根据两种不同 NAT 类型的特性,可以知道 IP 限制型 NAT 要比对称型 NAT 更容易穿越。要想让两者互通,突破口就是先将 IP 限制型 NAT 打通。因此,双方打洞的顺序非常关键,下面详细描述一下两台主机的打洞过程。
在 X 主机和 A 主机相互通信之前,它们都需要先与服务器通信,以交换必要的信息,如对方的外网 IP、端口等,并且在与服务器通信的过程中,它们同时会在各自的 NAT 上创建 NAT 映射表,如下所示:
// X 主机NAT 映射表
{
X 的内网IP,
X 的内网Port,
X 的外网IP,
X 的外网Port,
[S 的外网IP]
}
//A 主机NAT 映射表
{
A 的内网IP,
A 的内网Port,
A 的外网IP,
A 的外网Port,
S 的外网IP,
S 的外网Port
}

以上就是图 6.12 中的第❶步与第❷步完成的工作。紧接着执行第❸步,X主机向A主机发送数据。由于此时A主机的 NAT 映射表中没有X主机的记录,所以X主机送往A主机的数据因为无法穿越A主机的 NAT 而被A主机的 NAT 丢弃。
虽然X主机发送的数据无法穿越A主机的 NAT,但它却在X主机的 NAT 映射表上增加了A主机的 IP 地址,因此当执行图中的第❹步,即A主机向X主机发送数据时,数据就可以穿越NAT到达X主机了。与此同时,在A主机的 NAT 上也创建了与X主机相关的映射表。经此操作后,X主机与A主机所在 NAT 上的映射表变为下面的样子:
// X 主机 NAT 映射表
{
X 的内网IP,
X 的内网Port,
X 的外网IP,
X 的外网Port,
[S 的外网IP, A 的外网IP]
}
// A 主机 NAT 映射表
{
A 的内网IP,
A 的内网Port,
A 的外网IP,
A 的外网Port,
S 的外网IP,
S 的外网Port
}
{
A 的内网IP,
A 的内网Port,
A 的外网IP,
A 的外网Port,
X 的外网IP,
X 的外网Port
}
此时,由于A主机的 NAT 上已经打好了洞,X主机向A主机发送的数据就可以正常到达A主机了。同时,A主机也可以向X主机发送数据了。到这里,IP限制锥型 NAT 与对称型 NAT 就算打洞成功了。