原文地址:https://github.com/XTLS/Xray-core/discussions/237 ,作者有可能会不定期的更新,如感兴趣可去查看原文。
简单理解 IP Packet、TCP Connection、五元组、端口、User Datagram Protocol
这些是必须掌握的基本概念,实际上非常简单。
IP Packet:一个个符合 IP 协议的数据包,允许丢包,允许乱序(即接收时的顺序不同于发送时的顺序),属于非可靠传输。
它不是最底层的形式,这里无需深究,重点是知道它的特性。IP 数据包无法直接提供可靠传输,这对应用来说当然是很不方便的,于是就有了面向连接的 TCP 协议,它基于 IP 数据包,但实现了一套连接和可靠传输机制,大多数其它协议直接放 TCP 上面即可。
确定一个 TCP 连接的是“五元组”:
- TCP 协议本身的标识
- 自己的 IP 地址
- 自己使用的一个端口(Port)
- 对方的 IP 地址
- 对方使用的一个端口
TCP 连接建立后,双方便可从自己的端口向对方的端口发送应用数据,这是全双工的,即双方都可以一边发送数据、一边接收数据。
“端口”这个概念各位都不陌生,它常和 IP 一起出现,但实际上它不属于 IP 协议(没想到吧.jpg),它属于更上层的 TCP、UDP 协议。TCP、UDP 是分别实现了“端口”这一标识方式,所以这两个协议的“端口”不会互相影响。 ping
用到的协议是 ICMP,它也是基于 IP 数据包,与 TCP、UDP 是类似的,但 ICMP 就没有“端口”这个概念。顺便提一句,常见的代理协议只能代理 TCP 或加个 UDP,不能代理 ICMP,所以就无法 ping
,有此需求请用传统的 VPN。
接下来终于轮到我们的主角登场了:UDP(User Datagram Protocol)
虽然 UDP 和 TCP 一样是基于 IP 数据包的,但它异常简单,直接完全继承了 IP 数据包的特性:
- 允许丢包
- 允许乱序
- Apparently,属于非可靠传输
你可以这样简单理解:UDP 协议就只是在 IP 协议的基础上加了一个端口机制和校验而已。
对于 UDP 而言,TCP 的“连接”机制也是不存在的,即你申请到一个本地 UDP 端口后,不需要握手/建立连接即可直接向任意 IP 的任意 UDP 端口发送应用数据。 不需要关心对方有没有收到数据,对方也不会告诉你有没有收到数据。(需要指出:存在一种 connected UDP 的中间状态,这只是在发送数据前确定一下目标地址,并没有真正去握手)
UDP 的这些特性催生了三种应用方式:
- 注重效率,比如 DNS 查询(不需要先握手)
- 注重实时,比如直播、语音等(允许丢包,不需要等重传)
- 两者皆有 + P2P,比如一些联机游戏、语音等。注意这就是充分利用了上面加粗的那种 UDP 特性,也是本文的重点。
当然还有另一种对 UDP 的应用方式:基于 UDP 造新的通用可靠传输协议,比如 KCP、QUIC。为什么这些新协议不直接基于 IP 协议而要基于 UDP 协议?因为前者往往需要各级运营商进行设备、系统改造来支持,这显然不太现实,所以 UDP 成了更合适的选择。
那么 FullCone、Symmetric 又是什么?
这两个指的都是 NAT 行为,NAT 的全称为 Network Address Translation
,就是你家路由器、各级运营商做的事情:地址转换。NAT 的广泛存在是因为 IPv4 地址不足,另一方面它还可以保护局域网中的设备。
对于 TCP 而言,NAT 行为是什么并不重要,因为 TCP 是双向的流,本机每发起一个 TCP 连接往往会使用一个新的临时端口,从而对应一个新的五元组。
但对于 UDP,NAT 行为可太重要了,因为 UDP 是方向不定的包,使用同一个本地 UDP 端口向不同的目标二元组发包十分常见。
二元组:IP 和 Port,任一不同即视为不同的二元组
那么这种情况下 UDP 数据包到达路由器后,路由器要怎样转发它呢?这就取决于路由器的 NAT 行为了。
其实 NAT 行为多种多样,这里先举例介绍最具代表性的情况。首先有以下情景:
- 本地来源二元组 A 向远端目标二元组 M 发若干个包
- 本地来源二元组 A 又向远端目标二元组 N 发若干个包
如果由于目标二元组不同,路由器把 A->M、A->N 分别映射成了自己的 A1->M、A2->N(一般为分别使用两个不同的端口发包),且严格限制回包来源,就属于 Symmetric。不难发现,这时候实际上变成了类似 TCP “连接”的通信模式,也是大多数运营商的做法。
而若路由器只看来源二元组 A,始终映射成自己的 A1 向 M、N 发包,就属于 Cone NAT;更进一步,如果 A1 收到了回包,路由器不管来源,直接把这个包发回给 A,就属于 FullCone,也是代理类软件能实现的最佳 NAT 等级、P2P 游戏必备神器(GTA NAT 开放)
上面是简单举例,现实中运营商大概率不会主动分配给你一个公网 IP,也就是说还需要经过层层 NAT,最终得到 Symmetric 很正常。所以为什么你用了代理协议,比如 Xray-core 的 Shadowsocks、Trojan 就可以获得 FullCone?
很简单,因为此时用到的是你的 VPS 的公网 IP,和你本地的 NAT 环境没有任何关系。
这就是为什么要特殊设置 VPS 的防火墙:它默认会过滤返回的包的来源,导致你只能得到某种 Restricted Cone 而不是 FullCone。
对于简单的 UDP 需求比如 DNS 查询,Symmetric 也不是不能用。但对于复杂的 UDP 需求,比如各类 P2P 场景,实现 FullCone 就非常重要了,因为应用程序需要对外使用一个固定的端口,通过这个端口不受限制地往任何目标发包、从任何目标收包(至少别测错了当前的 NAT 类型,后文会说明)。如果你主要是为了打游戏,可以让 UDP 走 SS 协议,因为它拥有原生 UDP 的特性。
这里提一下,Xray-core 正计划着推出更适合打游戏的协议。
Xray-core 和一些代理协议中的 UDP 细节讲解
前面都是铺垫,终于到主菜了。
Xray-core 同时支持 FullCone 和 Symmetric 两种模式,且对协议的支持也非常全面,是很理想的例子。
完美支持 FullCone 的有:
- Shadowsocks 入站、出站
- Trojan 入站、出站
- Socks 入站、出站
- Dokodemo-door TPROXY 入站(透明代理)
- Freedom 出站,支持域名解析
仅支持 Symmetric 的有:
- VMess,因为协议结构不支持,后面会说原因
- 当前的 Mux,同样是协议结构不支持
- VLESS(FullCone 在路上了)
众所周知 v2ray 对 UDP 的支持一言难尽,所以 Xray-core 是重构了相关架构和各个出入站的代码,外加反复测试和对很多细节问题的定位、修复,才实现了全面 FullCone 化(除非协议不支持,这种情况会为它准备没问题的 Symmetric,Clash 的 VMess 存在问题)
Xray-core 的 release note 都很有营养,再摘抄一段:
- Socks5、Shadowsocks 都是原生 UDP,它们的 UDP 不走底层传输方式
- VLESS、Trojan、VMess、Mux 都是 UDP over TCP,且走底层传输方式
- HTTP 出入站不支持代理 UDP,Socks 版本 5 之前也不支持 UDP
- 这里的 FullCone 指的是 UDP 的 NAT 行为,配置时尤其注意防火墙
- 链式代理若要实现 FullCone,一般来说所有环节都要支持 FullCone
- Docker 若要实现 FullCone,相关容器的网络模式需要是 Host
补充:Socks、SS 是原生 UDP,套 TLS/WSS 后它们的 UDP 并没有被特殊处理,除非开了 Mux。SS 的 SIP003 插件也不管 UDP。
UDP over TCP 简称 UoT,特别注意,即使你用 mKCP、QUIC 作为底层传输方式,UoT 的也并不会表现出原生 UDP 的那些特性。
v2ray-core 存在的问题
这里主要是解惑,让 UDP 不再玄学。
- v2ray-core 架构上只支持 Symmetric 路由,所以你用 v2ray-core 的任何协议都只能 Symmetric
- v2ray-core 各出入站对 UDP 的处理和 TCP 是类似的逻辑,不可能实现 UDP 特有的 FullCone
- v2ray-core 的 Freedom 出站收返回的包时却没有按 Symmetric 过滤来源,这是玄学的根本原因
- v2ray-core 中各处维持 UDP 映射关系的不活动超时时间都很短,所以很容易出现断流等情况
对于第 3 点,简单来说是这样:
- 正常的 Symmetric NAT,A 对 M 发过包,只能收到从 M 返回的包,其它的会被过滤掉
- 如果中间插一个 v2ray,即使 A 没对 N 发过包,A 也能收到 N 发过来的包
- 重点是此时 N 的地址被丢掉了,A 会以为这个包是 M 发给它的,绝无仅有的迷惑行为
这种行为是预期之外的,再加上一些不标准的测试服务器,就会导致能给 v2ray、VMess 测出 FullCone,实际上却完全不起作用。
去年七月底我在某个开发者群内说过这个问题(不标准的测试服务器比如 Google 的那些,以及 v2ray UDP 的迷惑行为),随后 NatTypeTester 的更新只保留了五个标准的测试服务器,并特意验证了返回的包的源地址,测 v2ray 会显示 UnsupportedServer。
此外,Google 的一些应用会先自己测一下当前网络的 NAT 类型,若测出了假的 FullCone,就会导致奇奇怪怪的问题。
NAT 行为进一步探究
相信你已经发现了,NAT 行为并不只有 FullCone、Symmetric 这两种(但这是最极端的两种),实际上 NAT 行为由“发包时映射”和“收包时过滤”这两个行为来共同确定,FullCone 就是两者都最开放,Symmetric 就是两者都最严格,引用一张图:
可以看到,RFC 3489 定义了四种经典的 NAT 行为,v2ray 实际上不属于其中的任何一种,但它最接近 RFC 5780 的 Address and Port-Dependent Mapping
加 Endpoint-Independent Filtering
,即图中的 NAT Type 7,只是可能会把返回的包的来源搞错。
Xray-core 的代理协议如何实现 FullCone
这是本文的核心内容,其实原理很简单:把你本地的一个 UDP 端口映射为 VPS 的一个 UDP 端口,并使它们具有相同的效果。
拿一个最简单的场景举例:Socks 入站 + Freedom 出站
- Socks 入站收到二元组 A 发来的 Socks UDP 包,其中包含原始载荷与其原始目标 M,路由到 Freedom
- Freedom 出站使用一个随机端口将原始载荷发到其原始目标 M,这里认为 Freedom 使用了二元组 A1
- 映射关系已经建立,一段时间内 Socks 入站又收到了 A 发来的代理包,Freedom 还会用 A1 发到目标
- 同样地,如果 Freedom 的 A1 收到了 N 发回的包,Socks 就会把原始载荷同 N 这个信息一起发回给 A
当然,FullCone 还需要调用方按常理使用 Socks 代理协议,诸如各种 tun2socks 实现一般是没问题的。
下面插一个 Shadowsocks 出入站进来:
- 路由到 Shadowsocks 出站,它也是使用一个随机端口,将加密后的“载荷与目标”发到服务端的 SS 入站
- 服务端 SS 入站收到了客户端 SS 出站发来的 Shadowsocks UDP 包,解密,剩下的流程和上面没有区别
Trojan 协议的 UDP 也是类似的原理,不同之处是每个来源二元组都会对应一条 TCP 连接,在 TCP 上传输 UDP 的“载荷与目标”。
那么为什么同样是 UoT 的 VMess 却无法实现 FullCone?
根本原因是 VMess 的 UoT 协议结构只能在最开始时传一个“目标”,后面的多个数据包只能传“载荷”而不能带“目标”,服务端会把后续的数据包都发往最开始的“目标”。服务端向客户端返回数据包是同理的,协议结构只有“载荷”,客户端会认为返回的数据包都来自于最开始的“目标”,这和 v2ray 设计上的问题倒是一脉相承的,自带的 Mux 当然也是这样。(VLESS 也没有幸免,不过在改了)
所以对于 VMess、Mux、VLESS,Xray-core 目前是按 Symmetric 来路由的。否则如果是 FullCone 模式,后面的包都会被发到第一个包的目标地址,这就是 Clash 的 VMess 存在的问题,但并不好解决。此外,存在此问题时 VMess 又会被测出 FullCone,假的。
透明代理 TPROXY UDP 的原理
为了让游戏机用上 Xray-core 实现 FullCone,通常需要一个 Linux 设备来透明代理,一个树莓派就可以搞定。
为什么透明代理 UDP 只能 TRPOXY 而不推荐 REDIRECT?
- REDIRECT 会修改 UDP 包的目标二元组,并且此时 Linux 没有提供一个配套的机制让代理软件获知 UDP 包的原目标地址
- TRPOXY 则完全相反:它不会修改 UDP 包,Linux 还提供了简单的配套机制让代理软件获知 UDP 包的原目标地址
等需要往回发包时,代理软件会先在本地伪造出“返回的 UDP 包的来源二元组”的 socket,用这个 socket 把包发回去(这个原理就是可能会遇到 too many open files
的原因)。相比于其它软件,Xray-core 对这里有专门的优化,更优雅且有更好的性能。
若你在用 Windows 测透明代理的 NAT,一定注意要把当前网络设为 专用网络,这是很多人踩过的大坑,我也踩过。
提一下 QUIC:启用了 Xray-core 的 XTLS 时,通往 UDP 443 端口的流量默认会被拦截(拦截 QUIC),这样应用就不会使用 QUIC 而会使用 TLS,XTLS 才会真正生效。实际上,QUIC 本身也不适合被代理,因为 QUIC 自带了 TCP 的功能,UoT 就相当于两层 TCP 了。