Switch-Router

P2P游戏通信原理与STUN/TURN协议笔记

Published at 2024-09-17 | Last Update 2024-09-17

本文用于记录个人学习P2P游戏通信过程中的心得体会。

两种网络拓扑 CS vs P2P

网络游戏一般采用两种网路拓扑之一: CS(Client-Sever) 或 P2P(Peer-to-Peer)

在采用 CS 网络拓扑的游戏中, 各个玩家客户端都是与中心服务器进行网络通信, 这类游戏的游戏逻辑处理和计算都在服务端, 典型的例子如 PUBG、LOL、DOTA2、CS2 等.

而 P2P 网络拓扑的游戏则不一样, 各个玩家通常是与一名玩家(房主)建立网络连接, 再由房主进行网络流量中转和分发。

这个过程中最重要的一环, 就是玩家与玩家之间要能够给通过网络进行直接通信.

如果玩家们都有公网IP,直接通信不是难事。然而, 现实中具备该条件的玩家毕竟是少数, 大部分玩家只能看到自己的内网 IP, 身处不同 NAT 环境之下的玩家间显然不能通过内网 IP 进行通信.

那么应该如何做呢?在这之前,我们需要先了解几种 NAT 类型。

不同的 NAT 类型 - RFC3489

尽管大部分人都身处 NAT 环境, 但 NAT 环境之间亦有不同。[RFC3489] 定义了下面几种 NAT 类型。

图中 PC1 位于内网, PC2\PC3 具有公网 IP。PC1 先主动与 PC2[IP2:PORT2]进行通信, 按照 NAT 设备的规范, NAT 设备会建立起内网地址[ip1:port1]到公网地址[IP1,PORT1]的映射。

注: 本文中, ip:port 表示内网地址端口, IP:PORT 表示公网地址端口

而从 IP2:PORT2 到 IP1:PORT1 的反向流量会被允许通过, 但 IP2:PORT2’ 和 PC3 到 PC1 的连通性在几种 NAT 环境下会有所区别。

注: 本文中 PORT2’ 表示不同于 PORT2 的端口

  • Full-cone NAT

这是最宽松的 NAT 类型。公网侧只要是目的地址端口为[IP1:PORT1]的流量都可以被 PC1 收到。换句话说, NAT 设备并不检查流量的源地址端口是否为[IP2:Port2]。

  • Restricted-cone NAT

该 NAT 类型稍微严格一些。公网侧的流量需要源地址为 IP2 才能被放行, 但对源端口没有要求, 因此从 PC2 发出的[IP2:PORT2’]->[IP1:PORT1]的流量亦可以被 PC1 收到。

  • Port-restricted cone NAT

该 NAT 类型又更严格一些。公网侧的流量需要源地址端口为 IP2:Port2 才能放行。

  • Symetric NAT

该 NAT 类型严格程度继续加强。对于前面几种 NAT 类型,从[ip1:port1]发出的报文, 不管目的地址端口是哪里, 都会被统一映射到[IP1:PORT1]。

而 Symetric NAT 则不一样, 它的公网映射结果还要看目的地址端口,如果目的地不同,映射结果就不同。

不同的 NAT 行为 - RFC4787

RFC4787 认为 RFC3489 对 NAT 分类在描述现实的 NAT 行为方面是不够的,因此它对 NAT 行为重新进行了分类:分类依据是内网侧复用同一地址端口后, NAT设备对到不同公网目标流量时的映射结果。

如上图所示, 内网侧地址端口 X:x 到目标 Y1:y1 的流量在 NAT 设备源地址端口被映射到 X1’:x1’。而当它再到目标 Y2:y2 的流量被映射为 X2’:x2’。

根据 X1’:x1’ 与 X2’:x2’ 的关系, RFC4787 进行了如下分类:

  • Endpoint-Independent Mapping (目标无关映射)

X1’:x1’ 始终与 X2’:x2’ 相等。也就是说, 只要内网地址端口相同,不管目标地址端口如何,NAT 设备对源地址端口映射总能得到相同的结果。这种类型的 NAT 是最宽松的。

  • Address-Dependent Mapping (目标地址映射)

X1’:x1’ 与 X2’:x2’ 相等的条件时 Y1 等于 Y2。也就是说,当且仅当目标地址相同时,NAT设备才将内网地址端口映射为同一个公网地址端口

  • Address and Port-Dependent Mapping (目标地址端口映射)

X1’:x1’ 与 X2’:x2’ 相等的条件时 Y1:y1 等于 Y2:y2。也就是说,当且仅当目标地址相同时,NAT设备才将内网地址端口映射为同一个公网地址端口

STUN 协议 RFC8489

STUN (Session Traversal Utilities for NAT)是一个用于 NAT 穿越的工具型基础协议。它自身并不是一个独立的 NAT 穿越方案, 而是作为 NAT 穿越方案中的一部分。

严格来说,STUN 协议是一个 Client-Sever 协议,Client 和 Server 之间互相收发 Stun Message。

概念与术语

STUN 协议有一些重要的概念或术语,定义如下:

  • STUN client

内嵌在应用客户端中,可以发起 STUN request 并接收 STUN response。除此之外, 它还可以向 STUN server 发送 indication。indication 与 request 不同之处是 它不需要 response。

  • STUN server

游戏服务器开放一个公开端口(通常是3478), 接收 STUN client 发送的 STUN request 并回复 STUN response。它还可以向 SUN client 发送 indication。

  • Trasnport Address

表示IP地址和端口的组合。

  • Reflexive Transport Address

Reflexive 中文直译过来是”反射”, 在 STUN 协议中, Reflexive Transport Address 指的是 STUN server 看到的 STUN client 的公网地址端口.

  • Mapped Address

与 Reflexive Transport Address 意义相同, 这个概念只是由于历史原因才保留下来

  • Attribute

以 TLV 形式存在于 STUN 报文的 payload 中. 举个例子, 上面的 Reflexive Transport Address 就常常存在于 STUN reponse 中.

报文格式

STUN 协议报文存在于 UDP (TCP也可, 但用得不多) 的 payload 中。它自身是 header + payload 的组织形式.

其中 header 部分的格式如下

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |0 0|     STUN Message Type     |         Message Length        |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                         Magic Cookie                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               |
 |                     Transaction ID (96 bits)                  |
 |                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里最重要的就是 STUN Message Type. 它包含了报文的 type + method

以 type 来看, 协议定义了以下四种:

   #define IS_REQUEST(msg_type)       (((msg_type) & 0x0110) == 0x0000)
   #define IS_INDICATION(msg_type)    (((msg_type) & 0x0110) == 0x0010)
   #define IS_SUCCESS_RESP(msg_type)  (((msg_type) & 0x0110) == 0x0100)
   #define IS_ERR_RESP(msg_type)      (((msg_type) & 0x0110) == 0x0110)

对于 Request 来说, method 目前就只有 Binding 一种. 该 method 是 STUN 协议的核心.

   0x000: Reserved
   0x001: Binding
   0x002: Reserved; was SharedSecret prior to [RFC5389]

一个典型的 Binding Response 报文如下, 其中 MAPPED_ADDRESS 即为 STUN client 经过 NAT 设备后的公网地址端口

STUN 工作流程

以下图为例, 处于不同 NAT 环境下的两台内网 PC 需要建立 P2P 通信通道.

它们需要与 STUN Server 都进行通信, 获取自己的公网地址, 然后再建立互相之间的 P2P 通道。

1.每个 STUN client 分别向 STUN server 发送 Binding Request,此时在它们的 NAT 设备上会创建映射表项。

2.STUN server 在收到 Binding Request 后,顺利的话, 它将回复 Binding Response 给每个 STUN client,其中包含了 STUN client 各自的公网地址端口. 此时, STUN client 就获知了自己被 NAT 之后的公网地址端口, 另外各自的 NAT 设备上也建立了一条映射关系.

3.通过某个带外通道,通信双方交换各自的内网地址端口和公网地址端口给对方. 这里交换信息的方法不在 STUN 协议的范围内。

4.STUN client 1 和 STUN client 2 互相以对方的内网地址端口为目标发送 Binding Request. 在我们的例子中, STUN client 1 和 STUN client 2 位于不同的 NAT 下, 因此双发以内网地址端口为目标的消息不会收到任何响应

5.STUN client 1 和 STUN client 2 互相以对方 NAT 后的公网地址端口为目标发送 Binding Request, 该消息到达 NAT 设备时, 会建立相应的映射表项

6.当其中一方发送的 Bind Request 到达对方 NAT 设备时,可能对方上一步表项还没有建立(双发发送 Binding Request 是一个独立的过程)。 如果该 NAT 类型是 Full-cone, 则消息应该可以通过(满足第 1 步建立的表项)。如果是其他类型, 则会被丢弃, 不过这也没有关系, 因为当对方的 Bind Request 发出后, 本方的 Bind Request 也就能通过了

7.当双方的 NAT 设备都建立表项后, 如果 NAT 映射具有 Endpoint-Independent Mapping 行为, 则双方的 Binding Request 都可以抵达对方 STUN client。此时双方的 P2P 通道就建立完成.

打洞失败的场景

前面的展示过程中,打洞成功依赖 NAT 映射具有 Endpoint-Independent Mapping 行为。但如果不是呢, 比如 Address-Dependent Mapping, 我们可以通过分析看到, 此时对端发送的 Binding Request 依然不能穿越本端 NAT 设备

因此,对于这种场景,还需要其他协议。

TURN 协议 RFC8656

TURN (Traversal Using Relays around NAT) 就是这样一个协议。它的核心是Realy, 也就是用一个中间节点进行中继P2P流量。

概念与术语

TURN 协议建立在 STUN 协议的基础之上,因此, STUN 协议中定义的概念和属于均适用于 TURN 协议。除此之外,TURN 协议还有额外的概念。

  • TURN client

地位等同于 STUN client,是实现了 TURN 协议的 STUN client

  • TURN server

地位等同于 STUN server,是实现了 TURN 协议的 STUN server,同样一般使用 3478 端口。

  • Relayed Transport Address

中继地址端口。由 TURN server 分配的地址端口,分配成功后,peer 可以通过该地址端口与本端 client 进行通信

报文格式

TURN 消息的格式与 STUN 消息格式完全一致,内容上则是在 STUN 消息上做了一些扩充,比如支持了新的 method 和 Attribute。

比如核心 method: Allocate。TURN client 向 TURN server 发送 Allocate Request,接收 Allocate Response。其中 Response 里包含名为 Relayed Transport Address 的 Attribute,其值即为中继地址

典型例子

这里就以 RFC 中的例子举例说明:

图中 TURN client 与 peerA 与 peerB 分别建立 P2P 连接, 两个 peer 的区别是一个在 NAT 环境中,而另一个则直接拥有公网地址。

需要特别注意的是: 该例子中的 192.0.2.0/24 网段是被视为公网地址,尽管现实中,这是一个内网地址。

另外, 例子中的 NAT ,我们假定它是一个 “bad” NAT,比如是一个具有 address-and-port-dependent mapping 的 NAT, 否则我们完全用不上 TURN 协议,有 STUN 协议足够了。

TURN 协议的工作流程如下 (假设 Client 与 Peer B 建立 P2P):

TURN client 向 TURN server 发送 Allocate Request。意为:”请为我与 Peer B 的 P2P 通信提供一个中继(relay)地址”

TURN server 向 TURN client 回复 Allocate Reponse, 里面携带了 Relayed Transport Address。意为: “已经分配好了 192.0.2.15:50000, peer B 可以通过该地址端口与你通信”

随后通过带外通道,Peer B 也获取该中继地址。

此后, TURN client 向 Peer B 方向传输数据封装在目标为 192.0.2.15:3478 的消息中, 在 TURN server 内部转换为中继地址 192.0.2.15:50000, 再尤其转发给 Peer B。

从相反方向看,Peer B 向中继地址 192.0.2.15:50000 发送数据, 然后被 TURN server 内部转换为 192.0.2.15:3478, 再到 TURN client

参考资料

RFC3489 RFC4787 RFC8489 RFC8656 STUNTMAN What Is STUN?