- 创建者: 虚拟的现实,上次更新时间:11月 02, 2024 需要 3 分钟阅读时间
简单理解 IP Packet、TCP Connection、五元组、端口、User Datagram Protocol
这些是必须掌握的基本概念,实际上非常简单。
- IP Packet:一个个符合 IP 协议的数据包,允许丢包,允许乱序(即接收时的顺序不同于发送时的顺序),属于非可靠传输。
它不是最底层的形式,这里无需深究,重点是知道它的特性。IP 数据包无法直接提供可靠传输,这对应用来说当然是很不方便的,于是就有了面向连接的 TCP 协议,它基于 IP 数据包,但实现了一套连接和可靠传输机制,大多数其它协议直接放 TCP 上面即可。
确定一个 TCP 连接的是“五元组”:
- TCP 协议本身的标识
- 自己的 IP 地址
- 自己使用的一个端口(Port)
- 对方的 IP 地址
- 对方使用的一个端口
TCP 连接建立后,双方便可从自己的端口向对方的端口发送应用数据,这是全双工的,即双方都可以一边发送数据、一边接收数据。
“端口”这个概念它常和 IP 一起出现,但实际上它不属于 IP 协议,它属于更上层的 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 端口向不同的目标二元组发包十分常见。
那么这种情况下 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 的一些应用会先自己测一下当前网络的 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 了。
- 无标签
添加评论