Kenshin / simpread

简悦 ( SimpRead ) - 让你瞬间进入沉浸式阅读的扩展
http://ksria.com/simpread
GNU General Public License v3.0
8.12k stars 529 forks source link

离线下载HTML、离线下载MD的时候, 会卡「图片转换中,请稍等」(个别页面) #3230

Open justin201802 opened 2 years ago

justin201802 commented 2 years ago

网址: https://v2xtls.org/%E8%BF%9B%E9%98%B6%E5%BF%85%E8%AF%BB%EF%BC%9A%E4%BB%A3%E7%90%86%E5%8D%8F%E8%AE%AE-udp-%E5%85%A8%E6%96%B9%E4%BD%8D%E9%80%8F%E5%BD%BB%E8%A7%A3%E6%9E%90/

我自动化里面设置了,自动下载离线HTML和离线MD,打开上述网址后,offlinehtml的图片转base64是正常的, 但是解析出来的MD如下

"> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [v2xtls.org](https://v2xtls.org/%E8%BF%9B%E9%98%B6%E5%BF%85%E8%AF%BB%EF%BC%9A%E4%BB%A3%E7%90%86%E5%8D%8F%E8%AE%AE-udp-%E5%85%A8%E6%96%B9%E4%BD%8D%E9%80%8F%E5%BD%BB%E8%A7%A3%E6%9E%90/)

> 今天 rprx 大佬在 Xray-core 的项目库里又发了雄文,全方位介绍 UDP 协议以及 Xray 中的细节,强烈建议一 … 继续阅读进阶必读:代理协议 UDP 全方位透彻解析

> 今天 rprx 大佬在 Xray-core 的项目库里又发了雄文,全方位介绍 UDP 协议以及 Xray 中的细节,强烈建议一览。最新版请参考原文:[https://github.com/XTLS/Xray-core/discussions/237](https://github.com/XTLS/Xray-core/discussions/237)

绝大多数人对代理协议中的 UDP 部分完全没概念,目前很多经验丰富的使用者甚至是开发者一遇到 UDP 就变成小白,导致大多数关于 UDP 的问题悬而不决。鉴于 UDP 正扮演着越来越重要的角色,却没有一篇文章讲代理协议中的 UDP,我干脆写了这篇文章。

这篇文章的目的就是扭转现状,让大家完全参透 UDP,以便更游刃有余地使用 [**Xray-core**](https://github.com/XTLS/Xray-core) 或其它代理类软件。 —— RPRX

> 转载本文需在开头附上链接,本文还会与时俱进更新内容。

简单理解 IP Packet、TCP Connection、五元组、端口、User Datagram Protocol
-----------------------------------------------------------

这些是必须掌握的基本概念,实际上非常简单。

IP Packet:一个个符合 IP 协议的数据包,允许丢包,允许乱序(即接收时的顺序不同于发送时的顺序),属于非可靠传输。

它不是最底层的形式,这里无需深究,重点是知道它的特性。IP 数据包无法直接提供可靠传输,这对应用来说当然是很不方便的,于是就有了面向连接的 TCP 协议,它基于 IP 数据包,但实现了一套连接和可靠传输机制,大多数其它协议直接放 TCP 上面即可。

确定一个 TCP 连接的是 “五元组”:

1.  TCP 协议本身的标识
2.  自己的 IP 地址
3.  自己使用的一个端口(Port)
4.  对方的 IP 地址
5.  对方使用的一个端口

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 数据包的特性:

1.  允许丢包
2.  允许乱序
3.  Apparently,属于非可靠传输

你可以这样简单理解:UDP 协议就只是在 IP 协议的基础上加了一个端口机制和校验而已。

对于 UDP 而言,TCP 的 “连接” 机制也是不存在的,**即你申请到一个本地 UDP 端口后,不需要握手 / 建立连接即可直接向任意 IP 的任意 UDP 端口发送应用数据。** 不需要关心对方有没有收到数据,对方也不会告诉你有没有收到数据。(需要指出:存在一种 connected UDP 的中间状态,这只是在发送数据前确定一下目标地址,并没有真正去握手)

UDP 的这些特性催生了三种应用方式:

1.  注重效率,比如 DNS 查询(不需要先握手)
2.  注重实时,比如直播、语音等(允许丢包,不需要等重传)
3.  两者皆有 + 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 行为多种多样,这里先举例介绍最具代表性的情况。首先有以下情景:

1.  本地来源二元组 A 向远端目标二元组 M 发若干个包
2.  本地来源二元组 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 都很有营养,再摘抄一段:

1.  Socks5、Shadowsocks 都是原生 UDP,它们的 UDP 不走底层传输方式
2.  VLESS、Trojan、VMess、Mux 都是 UDP over TCP,且走底层传输方式
3.  HTTP 出入站不支持代理 UDP,Socks 版本 5 之前也不支持 UDP
4.  这里的 FullCone 指的是 UDP 的 NAT 行为,配置时尤其注意防火墙
5.  链式代理若要实现 FullCone,一般来说所有环节都要支持 FullCone
6.  Docker 若要实现 FullCone,相关容器的网络模式需要是 Host

补充:Socks、SS 是原生 UDP,套 TLS/WSS 后它们的 UDP 并没有被特殊处理,除非开了 Mux。SS 的 SIP003 插件也不管 UDP。

UDP over TCP 简称 UoT,特别注意,**即使你用 mKCP、QUIC 作为底层传输方式,UoT 的也并不会表现出原生 UDP 的那些特性。**

### v2ray-core 存在的问题

这里主要是解惑,让 UDP 不再玄学。

1.  v2ray-core 架构上只支持 Symmetric 路由,所以你用 v2ray-core 的任何协议都只能 Symmetric
2.  v2ray-core 各出入站对 UDP 的处理和 TCP 是类似的逻辑,不可能实现 UDP 特有的 FullCone
3.  v2ray-core 的 Freedom 出站收返回的包时却没有按 Symmetric 过滤来源,这是玄学的根本原因
4.  v2ray-core 中各处维持 UDP 映射关系的不活动超时时间都很短,所以很容易出现断流等情况

对于第 3 点,简单来说是这样:

1.  正常的 Symmetric NAT,A 对 M 发过包,只能收到从 M 返回的包,其它的会被过滤掉
2.  如果中间插一个 v2ray,即使 A 没对 N 发过包,A 也能收到 N 发过来的包
3.  重点是此时 N 的地址被丢掉了,A 会以为这个包是 M 发给它的,绝无仅有的迷惑行为

**这种行为是预期之外的,再加上一些不标准的测试服务器,就会导致能给 v2ray、VMess 测出 FullCone,实际上却完全不起作用。**

去年七月底我在某个开发者群内说过这个问题(不标准的测试服务器比如 Google 的那些,以及 v2ray UDP 的迷惑行为),随后 [NatTypeTester](https://github.com/HMBSbige/NatTypeTester) 的更新只保留了五个标准的测试服务器,并特意验证了返回的包的源地址,测 v2ray 会显示 UnsupportedServer。

此外,Google 的一些应用会先自己测一下当前网络的 NAT 类型,若测出了假的 FullCone,就会导致奇奇怪怪的问题。

### NAT 行为进一步探究

相信你已经发现了,NAT 行为并不只有 FullCone、Symmetric 这两种(但这是最极端的两种),实际上 NAT 行为由 “发包时映射” 和“收包时过滤”这两个行为来共同确定,FullCone 就是两者都最开放,Symmetric 就是两者都最严格,引用一张图:

[![](https://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gif)](https://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gif)

可以看到,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 出站

1.  Socks 入站收到二元组 A 发来的 Socks UDP 包,其中包含原始载荷与其原始目标 M,路由到 Freedom
2.  Freedom 出站使用一个随机端口将原始载荷发到其原始目标 M,这里认为 Freedom 使用了二元组 A1
3.  映射关系已经建立,**一段时间内 Socks 入站又收到了 A 发来的代理包,Freedom 还会用 A1 发到目标**
4.  同样地,如果 Freedom 的 A1 收到了 N 发回的包,Socks 就会把原始载荷同 N 这个信息一起发回给 A

当然,FullCone 还需要调用方按常理使用 Socks 代理协议,诸如各种 tun2socks 实现一般是没问题的。

下面插一个 Shadowsocks 出入站进来:

1.  路由到 Shadowsocks 出站,它也是使用一个随机端口,将加密后的 “载荷与目标” 发到服务端的 SS 入站
2.  服务端 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?

1.  REDIRECT 会修改 UDP 包的目标二元组,并且此时 Linux 没有提供一个配套的机制让代理软件获知 UDP 包的原目标地址
2.  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 了。

### 最后,仓库右上角点个 Star,谢谢!"

上述md中的

[![](https://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gif)](https://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gif)

被下列代码处理后

        function d(e, t) {
            M = "markdown",
            C = t,
            S = e,
            _ = new Map;
            var n = S.match(/!\[\]\(http\S+\)/gi);
            n && n.length > 0 ? (n.forEach(function(e, t) {
                S = S.replace(e, "![][img-" + t + "]"),
                e = e.replace(/[!\[\]\(]|[\)]/gi, ""),
                S = S + "\r\n\r\n" + ("[img-" + t + "]:" + e),
                _.set(e, "[img-" + t + "]:" + e)
            }),
            A = [].concat(r(_.keys())),
            E = A.length,
            k = 0,
            u(A[0])) : C(S)
        }

其中A[0]就变成了

https://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gifhttps://user-images.githubusercontent.com/63339210/107102123-5ed9d900-6811-11eb-9c00-6165ed9c18a2.gif

还有一个奇怪的地方在于, 根据我的debug顺序, 离线html的图片请求正确发出, url也正确, 正确返回。 但实际离线html的文件没有被生成, 而离线md的图片url有问题, 发出后返回403错误, 但是, 离线md的文件反而正常生成了。

justin201802 commented 2 years ago

分别测试了自动化只做某一项转换, 结果如下:

  1. 只自动化离线HTML, 图片转base64的url正确, 最终也能正常生成本地文件
  2. 只自动化离线md, 图片转base64的url还是重复,但最终也能正常生成本地文件

BTW: 这个卡“转换中”的问题, 我之前遇到过好几次, 不仅仅是现在给的这个url, tg群里面也有一位朋友遇到过类似的问题。