mirror of
https://github.com/e1732a364fed/v2ray_simple.git
synced 2025-10-07 01:32:57 +08:00
由新的udp架构重新设计udp分离信道传输方式和vless v1协议并初步实现代码
This commit is contained in:
221
docs/vless_v1.md
221
docs/vless_v1.md
@@ -1,211 +1,88 @@
|
|||||||
# vless v1
|
# vless v1 标准
|
||||||
|
|
||||||
目前v1仍然处于研发当中。建议先用v0,等v1完全出炉了再说,本文只是理论探索,实际代码暂未完整实现所有的设计.
|
本v1标准参考v0标准,并着重针对addon和udp部分进行了一些改动。原标准请参考
|
||||||
## 握手协议格式
|
|
||||||
|
|
||||||
具体我的探讨的一部分还可以查看 https://github.com/v2fly/v2ray-core/discussions/1655
|
https://github.com/v2ray/v2ray-core/issues/2636
|
||||||
|
|
||||||
总的来说 vless v1 简化了一些流程, 并重点考虑 非多路复用的 udp over tcp的 fullcone实现。
|
v1的讨论 可以参考 [vless_v1_discussion](vless_v1_discussion.md)
|
||||||
|
|
||||||
除了fullcone,我还想到了关于 内层加密, 连接池,以及 自动dns的 对协议的改进,请阅读全文了解详情。
|
本文为实际确定的具体标准,与讨论是有所不同的,讨论是非正式的。大家以本文为标准依据。
|
||||||
|
|
||||||
vless v0中服务端 的回复也是有数据头的,第一字节版本号,第二字节addon长度;而首先v2ray根本没有addon,所以第二字节总是0,
|
## 响应包包头的修改
|
||||||
|
|
||||||
而版本号的话,我约定服务端和客户端必须使用相同版本,
|
v1部分不再具有 v0中所定义的响应包头部分。tcp响应 直接就为承载数据,udp的话还是有特殊的定义,请看下文。
|
||||||
|
|
||||||
就是说,客户端请求使用v0,服务端不会返回一个v1出来;客户端请求v1,服务端也不会强制告诉客户端必须用v0,而是客户端要什么就是什么,
|
因为响应包没有包头,所以也不支持addon协商。如果服务端不支持某一addon 或者不支持v0/v1的话,应该无声地关闭底层连接,而不返回任何响应。
|
||||||
|
|
||||||
因此也不用再包含版本号,这样在v1中 服务端的回复就不必包数据头,可以减少内存操作,比v0更快。
|
## addon 含义修改
|
||||||
|
|
||||||
而且udp的话, 会有 crumfurs 模式以及 普通多路复用模式这两种模式, 所以还是要加以区分.所以v1的addon部分依然不能删掉;
|
对比:v0的 addon第一字节为addon长度, 剩余为 addon内容
|
||||||
不过我在这里做一些变化, v1 的addon第一字节不再是 addon长度, 而是addon类型,之后可以通过addon类型来判断addon长度.
|
|
||||||
|
|
||||||
在v1 传递udp握手时, addon第一字节为1时, 表示 “选择传输udp方式“,第二字节为0时表示使用普通多路复用模式(即与trojan相同), 第二字节为1时表示使用 crumfurs模式. 使用 crumfurs模式时, 单个通道不会再传输 raddr,因为仅用于单个raddr的信息传输.
|
v1发生变化, addon 第一字节 为addon类型指示, 剩余内容为响应类型的具体数据.
|
||||||
|
|
||||||
所以v1的 tcp读写特别简单,一旦握手成功,不会有任何数据头产生,没有丝毫额外开销,直接直连 (与trojan相同)
|
0 为无addon
|
||||||
|
1 为 udp_multi 标志. 开启此标志后, udp使用分离信道方式传输.
|
||||||
|
2 为 【多addon】标志, 此时第二字节为 addon的数量, 然后 第三字节 为 第一个addon的类型指示, 之后是第一个addon的数据块(视addon类型而定, 比如 udp_multi标志就没有任何数据块),然后是第二个addon的类型指示,以此类推。
|
||||||
|
|
||||||
传输udp数据 还是要有数据长度头的,这是udp的性质决定的,udp是基于包的,所以必须带长度头 来标明边界
|
v1不再采用protobuf,因为我们不应受制于任何复杂外部实现. 我们应该自有结构。
|
||||||
|
|
||||||
|
vless v0的addon主要被xray用来自定义xtls流控,而因为本作作者已经发现xtls的漏洞并摒弃xtls,开发了lazy技术,所以也不会去继承xray的现有addon。
|
||||||
|
|
||||||
但是因为websocket的数据包实际上首部就是带长度的,而且它这是在tls内部的,就和我们直接写数据长度一样
|
## udp的 多路复用 与 分离信道
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc6455#page-27
|
vless v1准备同时支持两种模式,【经典的 类socks5/trojan 的多路复用】的方式,以及【分离信道】的方式。
|
||||||
|
|
||||||
https://www.zhihu.com/question/29916578
|
究竟使用哪一种由 客户端决定,服务端将同时支持二者。
|
||||||
|
|
||||||
所以,vless v1外面包websocket的话,是不需要再重新加长度的,加了就是多此一举。
|
客户端使用 addon 部分来标识。
|
||||||
|
|
||||||
所以,我规定 vless v1 的udp实现中,会判断连接本身,如果连接 使用的是websocket或者类似的本身就加了数据长度头的,则udp包无需再加长度。
|
具体请看下文
|
||||||
|
## udp包头确定
|
||||||
|
|
||||||
也就是说,v1不再是一个与底层连接无关的协议,判断是否有ws或者grpc这种高级协议的存在,对于这些进行判断,就可以进行优化。
|
vless 的v0 始终处于beta中,而且 【udp长度】的包头仅仅在 commit中所提及,根本没写在该beta 文档里
|
||||||
|
|
||||||
虽然udp加长度只是加了两字节,但这只是write部分,read部分的话,为了防止粘包,就加了buffer,确实是多了一层内存读写操作,所以能精简就要精简,毕竟视频直播视频聊天等这种使用udp的部分都是 流量大户。总之这种优化能视udp的使用用途有不同程度的性能提升
|
本作定义的 v1 对vless 的udp包头进行完整定义。
|
||||||
|
|
||||||
而且作为翻墙要素我们是推荐ws或grpc来配合cdn的,所以还真是有用
|
|
||||||
|
|
||||||
v1主要还是 隔离信道的 udp over tcp 的 fullcone 的实现属于重要的创新,上面提到的优化也就占20%的 v1协议的独特之处。请接着读下文查看fullcone部分
|
在经典传输时,每一个udp数据报 都采用如下格式
|
||||||
|
第 1、2字节 为 port。第3字节为 atype,然后是变长的addr内容,定义与tcp的握手的包头一致。接着是udp数据包长度的2字节,然后是承载数据。
|
||||||
|
|
||||||
|
在 分离信道传输时,
|
||||||
|
|
||||||
## 关于udp over tcp 的 vless 的 fullcone
|
一,首个客户端握手包的握手部分采用与tcp相同的格式
|
||||||
|
|
||||||
将目标地址放到一个map里,udp建立过的连接 也放到一个map里。然后第二次访问时查看map看有没有用过的,有的话就使用之前的连接
|
接着是udp数据包,前两字节为数据长度的2字节,然后是承载数据。
|
||||||
|
|
||||||
然而,这只实现了一半,只记录旧连接是不够的,因为需要把目标的地址也附带发送到客户端,不然的话,客户端是无法区分到底是谁发来的。
|
二,非首包的格式:
|
||||||
|
|
||||||
|
剩余数据包中,因为目标raddr已经确定,所以不再有 经典传输中的 port、atype、addr部分。
|
||||||
|
|
||||||
这就是udp和tcp的不同。tcp可以确保目标总是自己拨号的那个,而udp则是可以任意人都向你发信息
|
不过, 因为我们服务端发往 客户端的方向 需要传输 UMFURS 信息,所以我们需要一字节来指示本次数据包的意义。
|
||||||
|
|
||||||
总之,还是要通过升级vless到1版本来实现。
|
客户端 发往 服务端方向 的 数据格式:
|
||||||
|
|
||||||
具体的话,也不一定需要新cmd,直接在 CmdUDP时,使用不同的数据包格式即可,可以参考socks5和trojan的格式标准
|
两字节数据包长度,然后跟着承载数据。
|
||||||
|
|
||||||
|
服务端 发往 客户端方向 的 数据格式:
|
||||||
|
|
||||||
比如trojan的: https://trojan-gfw.github.io/trojan/protocol
|
第一字节为意义指示,
|
||||||
|
|
||||||
|
0标识 普通 来自原始raddr的数据包,此时 第二字节和第三字节为数据长度,然后跟着数据;
|
||||||
|
|
||||||
## udp fullcone的信息传输过程
|
1标识 UMFURS 数据, 此时,第 2、3字节 为 port。第4字节为 atype,然后是变长的addr内容,定义与tcp的握手的包头一致。接着是udp数据包长度的2字节,然后是承载数据。
|
||||||
|
|
||||||
tcp代理是不需要fullcone的,也不可能实现;而因为udp的特殊性质,可能需要fullcone
|
|
||||||
|
|
||||||
先观察正常的tcp代理请求,发送一个tcp请求链接到代理服务器,然后直接就会使用这个连接传输双向的数据,这是因为目标是单一的。
|
|
||||||
|
|
||||||
而udp(fullcone)的代理请求是较为复杂的,尽管客户端只是发送了到 一个特定的远程 udp地址的 数据请求,但是因为要实现fullcone, 代理服务器所传来的信息不一定仅仅是 客户端第一次请求的 那个远程地址 所传来的,而可能是新的其它 地址所传来的(参考NAT打洞);
|
# 其他情况
|
||||||
然后,在客户端接到这个新地址所发来的数据后, 客户端可能还会接着向这个新地址发送数据,而这时,代理服务器 需要能够判别,这个新地址是不是之前接受过信息的地址,如果是的话,则依然使用之前 代理服务器所使用的 端口进行发送,如果不是的话,那说明这个是无效信息(即,作为fullcone,某个端口(A) 只能向自己 第一个发送信息的 目的地址,以及 所有 曾经向 A 发送过来信息 的 远程地址 发送信息)
|
## 传输在websocket之上的情况
|
||||||
|
|
||||||
所以,随着udp代理的请求的时间推移,可发送信息的 远程地址会 呈增加的趋势,也就是说建立了与多个远程udp地址的 p2p链接。
|
之前还讨论过 在使用ws时免除udp长度信息的计划
|
||||||
|
而且同样作为高级层,grpc和quic因为传输的是流, 不满足包的特征,是无法免去长度信息的。
|
||||||
|
|
||||||
如果是xray那种使用mux的办法,就是复用一个相同的tcp链接 去传输 多个连续的可以目标不同的 udp连接。
|
仔细思考, 免除udp长度信息主要是为了避免读取端 进行额外缓存, 减少读取端的拷贝
|
||||||
|
|
||||||
如果是我们要实现的新方法,则不是mux,即 每一次udp请求 都要产生一个 tcp连接,然后进行handshake。
|
但是, 一旦实施的话,实际上会增加 发送端的 buf, 发送端需要一次额外拷贝到buf中, 再整体一起写入 websocket, 所以实际上 如果 把 【发送端和接收端】 作为一个 整体 来考虑的话, 该计划并没有减少任何 内存拷贝, 而且还增加了代码复杂度。
|
||||||
|
|
||||||
看起来,反倒比mux复杂?但是,因为这个是并发的,所以虽然延迟可能高一些,但是却提高了吞吐量,网速能快一些,适合一些视频流的情况
|
|
||||||
|
|
||||||
所以,我们在vless v1版本中,除了并发的udp fullcone实现,也是要实现多路复用的,这样兼顾游戏与视频;
|
|
||||||
|
|
||||||
为了避免抄袭嫌疑,我当然不会重新使用mux协议来实现多路复用,而是自己设计一个传输协议。不过为了兼容现有客户端,我早晚还是要添加mux的支持,再说。
|
|
||||||
|
|
||||||
|
|
||||||
实际上,我还在思索,为什么vless一定要放到tcp上呢,为什么一定要udp over tcp呢?不能仿照socks5,直接在udp上传呢?也许是为了防探测吧,毕竟正常网页浏览的流量都是tcp的。但是实际上,完全可以伪装成 迅雷下载或者微信视频流这种。也有人说可能是怕udp限流、QOS等,所以才出现的hysteria吧。
|
|
||||||
|
|
||||||
好像quic功能就是做这个的.
|
|
||||||
|
|
||||||
总之这个纯udp上的实现问题以后再讨论。先把 udp over tcp搞定。
|
|
||||||
|
|
||||||
之所以以后需要讨论纯udp的问题,可能是因为为了实现fullcone,实际情况会与 udp over tcp 有所不同
|
|
||||||
|
|
||||||
## 非多路复用的 udp over tcp的 fullcone实现
|
|
||||||
|
|
||||||
xray的 vless的mux的fullcone的办法是,在vless里包一个mux协议,然后持续使用该tcp连接 来在内部传送多个udp请求
|
|
||||||
|
|
||||||
而我们将要在这里实现的办法是通用办法,即不复用tcp连接的办法, 即每一个tcp链接里只包含于一个固定的远程地址的通讯。
|
|
||||||
|
|
||||||
不过,问题出现在监听上,我们监听的话,是会随时主动向客户端程序发送 远程服务器传来的信息的,这时,客户端提交 请求是 一个请求地址 一个tcp连接,是一一对应的,但是我们 返回数据的话,是无法主动跟客户端 建立连接的,而且 返回的数据的地址不一定是 和请求所用的 tcp连接 一一对应, 因为有可能出现 新的 客户端不知道 的远程地址 向 客户端 发送信息
|
|
||||||
|
|
||||||
这就是和 socks5(或trojan)的不同;socks5和trojan在传udp时实际上都是多路复用,使用的是同一条 代理客户端和代理服务端 建立的链接 来传输多个地址可能不同的数据
|
|
||||||
|
|
||||||
所以,我的想法是,使用一种“居中”的办法,即不完全模仿socks5;
|
|
||||||
当 代理服务器收到的udp信息的来源是 客户端已知的来源时,直接使用之前 客户端用于向该地址发送请求 的tcp连接 将数据发回 客户端
|
|
||||||
而 当新udp信息的来源(A)时未知的时,使用一条单独的信道向客户端发送该数据;
|
|
||||||
|
|
||||||
然后如果客户端 有新的向这个 A 发送的 数据请求时,再向代理服务器发起新的tcp连接,然后该tcp连接 会被保存,等未来 该A 有新数据 想发送到客户端时,使用这个特定的tcp连接。
|
|
||||||
|
|
||||||
这种好处是,如果目标地址 我客户端之前向其 发送过信息,则会复用同一条tcp连接,专门与这个地址发送信息;
|
|
||||||
|
|
||||||
既不是多路复用,也不是纯每个请求都建立新tcp链接。
|
|
||||||
|
|
||||||
可以说是 “隔离信道”。
|
|
||||||
|
|
||||||
### 具体实现
|
|
||||||
|
|
||||||
那个单独的用于向客户端发送 “新远程地址链接” 的信道,是提前由 客户端主动向代理服务端建立好的一条信道。
|
|
||||||
|
|
||||||
暂且称为 “未知udp地址的信息的接收信道”, "a channel that receives udp messages from unknown remote source"
|
|
||||||
|
|
||||||
我这里简称 "CRUMFURS"(信道). 该特殊信息称为 "UMFURS" (信息)
|
|
||||||
|
|
||||||
这个信道是多路复用的,但是仅限于未知udp远程地址发来的信息,而且仅限于服务端向客户端发送数据。
|
|
||||||
|
|
||||||
在vless从未主动申请过 udp请求时,不必 特地向代理服务器请求建立 CRUMFURS, 因为没有udp的需求。
|
|
||||||
|
|
||||||
那么,就需要新增一条 命令,比如 Cmd_CRUMFURS
|
|
||||||
|
|
||||||
当客户端 发送的vless信息中 使用 Cmd_CRUMFURS 命令后,则该链接保持连接(keep alive),专门用于客户端接受新udp数据。
|
|
||||||
同时 此时服务端就意识到,客户端以后会产生各种udp需求,并做好一些相关准备。
|
|
||||||
|
|
||||||
|
|
||||||
然后客户端第一次 申请向 一个远程地址(A) 发送 udp请求时,还是使用普通的 CMD_UDP命令 新建连接,而且 如果客户端期待回复的话,则客户端也不关闭这条连接(也是keep alive的),然后,当客户端想要 第二次向 A发送数据时,使用这个之前建立好的tcp连接。监听来自A发来的信息 也是通过读取这个 建立好的tcp连接。
|
|
||||||
|
|
||||||
CMD_UDP 信息 的数据和v0协议的内容保持不变。
|
|
||||||
|
|
||||||
Cmd_CRUMFURS 指令 的信息,到指令这一项之后 就会完毕。理所当然,因为只是发送一条建立信道的指令,不包含其它信息。
|
|
||||||
|
|
||||||
服务端接收到 Cmd_CRUMFURS 指令后(当然也是要在验证uuid符合之后),会返回一字节 CRUMFURS_ESTABLISHED
|
|
||||||
|
|
||||||
UMFURS 信息内容:1字节atype,几字节的IP(视ip为ipv4和ipv6. 因为是远程未知udp信息,所以肯定不可能是域名),2字节的port,然后后面直接接 该信息的承载内容。所以这个信息长度至少为8(7字节头部和至少一字节数据)
|
|
||||||
|
|
||||||
|
|
||||||
服务端对 udp 信息的转发 的行为会与正常 symmetric NAT 不同。并不是对不同的 请求的远程目标地址都要 新 dial一遍udp。因为每dial一遍,都会 新使用 服务端的一个 新的随机udp端口,而我们为了实现fullcone,在传输 到 Cmd_CRUMFURS 里出现过的 目标地址(A) 的数据时,还是要使用最初 代理服务端 接收 (A)发送来 数据 所使用的 的服务器udp端口
|
|
||||||
|
|
||||||
所以,服务端 要对每一个dial产生的 UDPConn进行保存;然后, 在接到 客户端的 CMD_UDP 请求时,首先查看 下列情况
|
|
||||||
|
|
||||||
1. 以前是否向这个地址拨过号,如果拨过,直接从保存的列表中拿出来这个UDPConn,直接使用它继续发送数据
|
|
||||||
2. 如果没拨过号,那这个远程地址是否曾向 保存的 UDPConn 中 其中一个 发送过数据,如果能找到这个UDPConn,就使用这个
|
|
||||||
3. 如果自己没拨过号,也没有任何一个 UDPConn 接收 过 该远程地址的信息,则这个远程地址 属于新地址,我们就新Dial 一次。
|
|
||||||
|
|
||||||
所以,保存的机制就是一个map,以地址为 键,以 UDPConn为内容;
|
|
||||||
自己拨过的号所使用的地址(A)和该拨号产生的 UDPConn (U)作为一对 存入 map ;然后向(A)传来的任何其它未知地址(B)也和 (U)一起作为一对 存入 map
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
这个行为可以直接参考 direct/client.go 里的 RelayUDP_to_Direct(extractor proxy.UDP_Extractor) 函数。该函数就实现了fullcone
|
|
||||||
|
|
||||||
|
|
||||||
我规定,如果一个vless v1客户端 第一次想要发起 udp请求,必须先 发送 Cmd_CRUMFURS 命令
|
|
||||||
|
|
||||||
CRUMFURS 信道与普通UDP信道一样,要传 udp长度头。在应用ws或者grpc后则应不传,这个等我实现ws或grpc了再说
|
|
||||||
|
|
||||||
### 流量特征的避免
|
|
||||||
另外,为了避免 【同时具有两个tcp链接,且一个tcp链接只有入方向,没有出方向】 的流量特征,我们实际上不能让crumfurs成为真正的单独通道。
|
|
||||||
|
|
||||||
所以, 我们 crumfurs信息的传输要隐藏在普通链接中。 所以我们udp数据头部除了长度头之外,还要有一字节的 crumfurs标识,如果为0 则意味着没有 crumfurs信息产生,而如果为1的话,后面会附带crumfurs信息。
|
|
||||||
|
|
||||||
这样的话,也不用单独一个 crumfurs命令, 也不用单独拨号crumfurs信道。
|
|
||||||
|
|
||||||
### 普通udp转发与本作新创建方式的对比
|
|
||||||
|
|
||||||
业界普通udp的fullcone转发是使用 socks5/trojan 方式的这种多路复用方式,建立一个udp信道,然后传输多种udp数据;
|
|
||||||
|
|
||||||
而本作提出的 crumfurs方式实际上 是开辟了一种新的 单路udp转发方式.
|
|
||||||
|
|
||||||
此时从socks5读出的每一个udp链接,视其raddr而定,如果raddr是没遇到过的地址,则我们会新建立一个新信道
|
|
||||||
|
|
||||||
## 连接池
|
|
||||||
|
|
||||||
我们可以通过使用新协议命令来做到 非mux的 连接池技术,可以在每个vless 头部添加 “是否关闭连接”的信息
|
|
||||||
|
|
||||||
如果准备关闭连接,则实际上不关闭连接,而是标记这个连接进行备用,然后等客户端下一次想要创建一个新连接时,直接使用这个现有的连接。
|
|
||||||
|
|
||||||
与 多路mux的不同是,多路mux是有同时连接的上限的,而连接池技术则没有,所以还是会比多路mux的方法速度更快
|
|
||||||
|
|
||||||
|
|
||||||
## 服务端主动返回ip地址
|
|
||||||
|
|
||||||
我们访问vless时,是可以访问域名的,而此时客户端是 不知道该域名是否会被墙的;
|
|
||||||
|
|
||||||
如果有一种方法,可以让客户端知道该域名的真实ip(不能客户端自己查dns,因为可能被污染),那就好了。
|
|
||||||
|
|
||||||
如果我们在 vless v1的 服务端返回的首包的信息头中,头部首先返回该 目标的 实际 ip,这样就相当于客户端免费做了一次dns查询。
|
|
||||||
|
|
||||||
比如,头部 : 如果不返回ip,则第一字节为0;如果返回ip,则第一字节为1,然后第二字节为 ip类型,标识是v4还是v6,然后后面是实际ip数据,最后跟着的才是首包的实际数据
|
|
||||||
|
|
||||||
感觉这个方法不错,使用这种方法,则只需一个geoip数据库就可以判断 分流,不再需要额外的 geosite
|
|
||||||
|
|
||||||
|
|
||||||
## 内层加密
|
|
||||||
|
|
||||||
vmess的加密方式过于繁琐,而且如果怕封未知流量的话还是要套tls
|
|
||||||
|
|
||||||
有些小伙伴怕纯tls 容易被中间人攻击,那么我们如果内层也加密的话就不怕了
|
|
||||||
|
|
||||||
可以考虑推出一种简化版的加密方式. 当然显然,这个加密是可选的,而且默认不配置的话肯定我们是不加密的,因为vlessv1外面必套tls
|
|
||||||
|
|
||||||
vmess我觉得最复杂的地方就是客户端和服务端每次都要同时生成一大串数,很麻烦啊!
|
|
||||||
|
|
||||||
|
因此,讨论中的 免除udp长度信息的计划 不切实际,不予支持。
|
||||||
|
|
||||||
|
而且, v1的分离信道方式还是要先读取一字节, 所以还是要缓存, 所以完全没必要。
|
211
docs/vless_v1_discussion.md
Normal file
211
docs/vless_v1_discussion.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# vless v1 讨论
|
||||||
|
|
||||||
|
目前v1仍然处于研发当中。建议先用v0,等v1完全出炉了再说,本文只是理论探索,实际代码暂未完整实现所有的设计.
|
||||||
|
## 握手协议格式
|
||||||
|
|
||||||
|
具体我的探讨的一部分还可以查看 https://github.com/v2fly/v2ray-core/discussions/1655
|
||||||
|
|
||||||
|
总的来说 vless v1 简化了一些流程, 并重点考虑 非多路复用的 udp over tcp的 fullcone实现。
|
||||||
|
|
||||||
|
除了fullcone,我还想到了关于 内层加密, 连接池,以及 自动dns的 对协议的改进,请阅读全文了解详情。
|
||||||
|
|
||||||
|
vless v0中服务端 的回复也是有数据头的,第一字节版本号,第二字节addon长度;而首先v2ray根本没有addon,所以第二字节总是0,
|
||||||
|
|
||||||
|
而版本号的话,我约定服务端和客户端必须使用相同版本,
|
||||||
|
|
||||||
|
就是说,客户端请求使用v0,服务端不会返回一个v1出来;客户端请求v1,服务端也不会强制告诉客户端必须用v0,而是客户端要什么就是什么,
|
||||||
|
|
||||||
|
因此也不用再包含版本号,这样在v1中 服务端的回复就不必包数据头,可以减少内存操作,比v0更快。
|
||||||
|
|
||||||
|
而且udp的话, 会有 crumfurs 模式以及 普通多路复用模式这两种模式, 所以还是要加以区分.所以v1的addon部分依然不能删掉;
|
||||||
|
不过我在这里做一些变化, v1 的addon第一字节不再是 addon长度, 而是addon类型,之后可以通过addon类型来判断addon长度.
|
||||||
|
|
||||||
|
在v1 传递udp握手时, addon第一字节为1时, 表示 “选择传输udp方式“,第二字节为0时表示使用普通多路复用模式(即与trojan相同), 第二字节为1时表示使用 crumfurs模式. 使用 crumfurs模式时, 单个通道不会再传输 raddr,因为仅用于单个raddr的信息传输.
|
||||||
|
|
||||||
|
所以v1的 tcp读写特别简单,一旦握手成功,不会有任何数据头产生,没有丝毫额外开销,直接直连 (与trojan相同)
|
||||||
|
|
||||||
|
传输udp数据 还是要有数据长度头的,这是udp的性质决定的,udp是基于包的,所以必须带长度头 来标明边界
|
||||||
|
|
||||||
|
|
||||||
|
但是因为websocket的数据包实际上首部就是带长度的,而且它这是在tls内部的,就和我们直接写数据长度一样
|
||||||
|
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#page-27
|
||||||
|
|
||||||
|
https://www.zhihu.com/question/29916578
|
||||||
|
|
||||||
|
所以,vless v1外面包websocket的话,是不需要再重新加长度的,加了就是多此一举。
|
||||||
|
|
||||||
|
所以,我规定 vless v1 的udp实现中,会判断连接本身,如果连接 使用的是websocket或者类似的本身就加了数据长度头的,则udp包无需再加长度。
|
||||||
|
|
||||||
|
也就是说,v1不再是一个与底层连接无关的协议,判断是否有ws或者grpc这种高级协议的存在,对于这些进行判断,就可以进行优化。
|
||||||
|
|
||||||
|
虽然udp加长度只是加了两字节,但这只是write部分,read部分的话,为了防止粘包,就加了buffer,确实是多了一层内存读写操作,所以能精简就要精简,毕竟视频直播视频聊天等这种使用udp的部分都是 流量大户。总之这种优化能视udp的使用用途有不同程度的性能提升
|
||||||
|
|
||||||
|
而且作为翻墙要素我们是推荐ws或grpc来配合cdn的,所以还真是有用
|
||||||
|
|
||||||
|
v1主要还是 隔离信道的 udp over tcp 的 fullcone 的实现属于重要的创新,上面提到的优化也就占20%的 v1协议的独特之处。请接着读下文查看fullcone部分
|
||||||
|
|
||||||
|
|
||||||
|
## 关于udp over tcp 的 vless 的 fullcone
|
||||||
|
|
||||||
|
将目标地址放到一个map里,udp建立过的连接 也放到一个map里。然后第二次访问时查看map看有没有用过的,有的话就使用之前的连接
|
||||||
|
|
||||||
|
然而,这只实现了一半,只记录旧连接是不够的,因为需要把目标的地址也附带发送到客户端,不然的话,客户端是无法区分到底是谁发来的。
|
||||||
|
|
||||||
|
这就是udp和tcp的不同。tcp可以确保目标总是自己拨号的那个,而udp则是可以任意人都向你发信息
|
||||||
|
|
||||||
|
总之,还是要通过升级vless到1版本来实现。
|
||||||
|
|
||||||
|
具体的话,也不一定需要新cmd,直接在 CmdUDP时,使用不同的数据包格式即可,可以参考socks5和trojan的格式标准
|
||||||
|
|
||||||
|
比如trojan的: https://trojan-gfw.github.io/trojan/protocol
|
||||||
|
|
||||||
|
## udp fullcone的信息传输过程
|
||||||
|
|
||||||
|
tcp代理是不需要fullcone的,也不可能实现;而因为udp的特殊性质,可能需要fullcone
|
||||||
|
|
||||||
|
先观察正常的tcp代理请求,发送一个tcp请求链接到代理服务器,然后直接就会使用这个连接传输双向的数据,这是因为目标是单一的。
|
||||||
|
|
||||||
|
而udp(fullcone)的代理请求是较为复杂的,尽管客户端只是发送了到 一个特定的远程 udp地址的 数据请求,但是因为要实现fullcone, 代理服务器所传来的信息不一定仅仅是 客户端第一次请求的 那个远程地址 所传来的,而可能是新的其它 地址所传来的(参考NAT打洞);
|
||||||
|
然后,在客户端接到这个新地址所发来的数据后, 客户端可能还会接着向这个新地址发送数据,而这时,代理服务器 需要能够判别,这个新地址是不是之前接受过信息的地址,如果是的话,则依然使用之前 代理服务器所使用的 端口进行发送,如果不是的话,那说明这个是无效信息(即,作为fullcone,某个端口(A) 只能向自己 第一个发送信息的 目的地址,以及 所有 曾经向 A 发送过来信息 的 远程地址 发送信息)
|
||||||
|
|
||||||
|
所以,随着udp代理的请求的时间推移,可发送信息的 远程地址会 呈增加的趋势,也就是说建立了与多个远程udp地址的 p2p链接。
|
||||||
|
|
||||||
|
如果是xray那种使用mux的办法,就是复用一个相同的tcp链接 去传输 多个连续的可以目标不同的 udp连接。
|
||||||
|
|
||||||
|
如果是我们要实现的新方法,则不是mux,即 每一次udp请求 都要产生一个 tcp连接,然后进行handshake。
|
||||||
|
|
||||||
|
看起来,反倒比mux复杂?但是,因为这个是并发的,所以虽然延迟可能高一些,但是却提高了吞吐量,网速能快一些,适合一些视频流的情况
|
||||||
|
|
||||||
|
所以,我们在vless v1版本中,除了并发的udp fullcone实现,也是要实现多路复用的,这样兼顾游戏与视频;
|
||||||
|
|
||||||
|
为了避免抄袭嫌疑,我当然不会重新使用mux协议来实现多路复用,而是自己设计一个传输协议。不过为了兼容现有客户端,我早晚还是要添加mux的支持,再说。
|
||||||
|
|
||||||
|
|
||||||
|
实际上,我还在思索,为什么vless一定要放到tcp上呢,为什么一定要udp over tcp呢?不能仿照socks5,直接在udp上传呢?也许是为了防探测吧,毕竟正常网页浏览的流量都是tcp的。但是实际上,完全可以伪装成 迅雷下载或者微信视频流这种。也有人说可能是怕udp限流、QOS等,所以才出现的hysteria吧。
|
||||||
|
|
||||||
|
好像quic功能就是做这个的.
|
||||||
|
|
||||||
|
总之这个纯udp上的实现问题以后再讨论。先把 udp over tcp搞定。
|
||||||
|
|
||||||
|
之所以以后需要讨论纯udp的问题,可能是因为为了实现fullcone,实际情况会与 udp over tcp 有所不同
|
||||||
|
|
||||||
|
## 非多路复用的 udp over tcp的 fullcone实现
|
||||||
|
|
||||||
|
xray的 vless的mux的fullcone的办法是,在vless里包一个mux协议,然后持续使用该tcp连接 来在内部传送多个udp请求
|
||||||
|
|
||||||
|
而我们将要在这里实现的办法是通用办法,即不复用tcp连接的办法, 即每一个tcp链接里只包含于一个固定的远程地址的通讯。
|
||||||
|
|
||||||
|
不过,问题出现在监听上,我们监听的话,是会随时主动向客户端程序发送 远程服务器传来的信息的,这时,客户端提交 请求是 一个请求地址 一个tcp连接,是一一对应的,但是我们 返回数据的话,是无法主动跟客户端 建立连接的,而且 返回的数据的地址不一定是 和请求所用的 tcp连接 一一对应, 因为有可能出现 新的 客户端不知道 的远程地址 向 客户端 发送信息
|
||||||
|
|
||||||
|
这就是和 socks5(或trojan)的不同;socks5和trojan在传udp时实际上都是多路复用,使用的是同一条 代理客户端和代理服务端 建立的链接 来传输多个地址可能不同的数据
|
||||||
|
|
||||||
|
所以,我的想法是,使用一种“居中”的办法,即不完全模仿socks5;
|
||||||
|
当 代理服务器收到的udp信息的来源是 客户端已知的来源时,直接使用之前 客户端用于向该地址发送请求 的tcp连接 将数据发回 客户端
|
||||||
|
而 当新udp信息的来源(A)时未知的时,使用一条单独的信道向客户端发送该数据;
|
||||||
|
|
||||||
|
然后如果客户端 有新的向这个 A 发送的 数据请求时,再向代理服务器发起新的tcp连接,然后该tcp连接 会被保存,等未来 该A 有新数据 想发送到客户端时,使用这个特定的tcp连接。
|
||||||
|
|
||||||
|
这种好处是,如果目标地址 我客户端之前向其 发送过信息,则会复用同一条tcp连接,专门与这个地址发送信息;
|
||||||
|
|
||||||
|
既不是多路复用,也不是纯每个请求都建立新tcp链接。
|
||||||
|
|
||||||
|
可以说是 “隔离信道”。
|
||||||
|
|
||||||
|
### 具体实现
|
||||||
|
|
||||||
|
那个单独的用于向客户端发送 “新远程地址链接” 的信道,是提前由 客户端主动向代理服务端建立好的一条信道。
|
||||||
|
|
||||||
|
暂且称为 “未知udp地址的信息的接收信道”, "a channel that receives udp messages from unknown remote source"
|
||||||
|
|
||||||
|
我这里简称 "CRUMFURS"(信道). 该特殊信息称为 "UMFURS" (信息)
|
||||||
|
|
||||||
|
这个信道是多路复用的,但是仅限于未知udp远程地址发来的信息,而且仅限于服务端向客户端发送数据。
|
||||||
|
|
||||||
|
在vless从未主动申请过 udp请求时,不必 特地向代理服务器请求建立 CRUMFURS, 因为没有udp的需求。
|
||||||
|
|
||||||
|
那么,就需要新增一条 命令,比如 Cmd_CRUMFURS
|
||||||
|
|
||||||
|
当客户端 发送的vless信息中 使用 Cmd_CRUMFURS 命令后,则该链接保持连接(keep alive),专门用于客户端接受新udp数据。
|
||||||
|
同时 此时服务端就意识到,客户端以后会产生各种udp需求,并做好一些相关准备。
|
||||||
|
|
||||||
|
|
||||||
|
然后客户端第一次 申请向 一个远程地址(A) 发送 udp请求时,还是使用普通的 CMD_UDP命令 新建连接,而且 如果客户端期待回复的话,则客户端也不关闭这条连接(也是keep alive的),然后,当客户端想要 第二次向 A发送数据时,使用这个之前建立好的tcp连接。监听来自A发来的信息 也是通过读取这个 建立好的tcp连接。
|
||||||
|
|
||||||
|
CMD_UDP 信息 的数据和v0协议的内容保持不变。
|
||||||
|
|
||||||
|
Cmd_CRUMFURS 指令 的信息,到指令这一项之后 就会完毕。理所当然,因为只是发送一条建立信道的指令,不包含其它信息。
|
||||||
|
|
||||||
|
服务端接收到 Cmd_CRUMFURS 指令后(当然也是要在验证uuid符合之后),会返回一字节 CRUMFURS_ESTABLISHED
|
||||||
|
|
||||||
|
UMFURS 信息内容:1字节atype,几字节的IP(视ip为ipv4和ipv6. 因为是远程未知udp信息,所以肯定不可能是域名),2字节的port,然后后面直接接 该信息的承载内容。所以这个信息长度至少为8(7字节头部和至少一字节数据)
|
||||||
|
|
||||||
|
|
||||||
|
服务端对 udp 信息的转发 的行为会与正常 symmetric NAT 不同。并不是对不同的 请求的远程目标地址都要 新 dial一遍udp。因为每dial一遍,都会 新使用 服务端的一个 新的随机udp端口,而我们为了实现fullcone,在传输 到 Cmd_CRUMFURS 里出现过的 目标地址(A) 的数据时,还是要使用最初 代理服务端 接收 (A)发送来 数据 所使用的 的服务器udp端口
|
||||||
|
|
||||||
|
所以,服务端 要对每一个dial产生的 UDPConn进行保存;然后, 在接到 客户端的 CMD_UDP 请求时,首先查看 下列情况
|
||||||
|
|
||||||
|
1. 以前是否向这个地址拨过号,如果拨过,直接从保存的列表中拿出来这个UDPConn,直接使用它继续发送数据
|
||||||
|
2. 如果没拨过号,那这个远程地址是否曾向 保存的 UDPConn 中 其中一个 发送过数据,如果能找到这个UDPConn,就使用这个
|
||||||
|
3. 如果自己没拨过号,也没有任何一个 UDPConn 接收 过 该远程地址的信息,则这个远程地址 属于新地址,我们就新Dial 一次。
|
||||||
|
|
||||||
|
所以,保存的机制就是一个map,以地址为 键,以 UDPConn为内容;
|
||||||
|
自己拨过的号所使用的地址(A)和该拨号产生的 UDPConn (U)作为一对 存入 map ;然后向(A)传来的任何其它未知地址(B)也和 (U)一起作为一对 存入 map
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
这个行为可以直接参考 direct/client.go 里的 RelayUDP_to_Direct(extractor proxy.UDP_Extractor) 函数。该函数就实现了fullcone
|
||||||
|
|
||||||
|
|
||||||
|
我规定,如果一个vless v1客户端 第一次想要发起 udp请求,必须先 发送 Cmd_CRUMFURS 命令
|
||||||
|
|
||||||
|
CRUMFURS 信道与普通UDP信道一样,要传 udp长度头。在应用ws或者grpc后则应不传,这个等我实现ws或grpc了再说
|
||||||
|
|
||||||
|
### 流量特征的避免
|
||||||
|
另外,为了避免 【同时具有两个tcp链接,且一个tcp链接只有入方向,没有出方向】 的流量特征,我们实际上不能让crumfurs成为真正的单独通道。
|
||||||
|
|
||||||
|
所以, 我们 crumfurs信息的传输要隐藏在普通链接中。 所以我们udp数据头部除了长度头之外,还要有一字节的 crumfurs标识,如果为0 则意味着没有 crumfurs信息产生,而如果为1的话,后面会附带crumfurs信息。
|
||||||
|
|
||||||
|
这样的话,也不用单独一个 crumfurs命令, 也不用单独拨号crumfurs信道。
|
||||||
|
|
||||||
|
### 普通udp转发与本作新创建方式的对比
|
||||||
|
|
||||||
|
业界普通udp的fullcone转发是使用 socks5/trojan 方式的这种多路复用方式,建立一个udp信道,然后传输多种udp数据;
|
||||||
|
|
||||||
|
而本作提出的 crumfurs方式实际上 是开辟了一种新的 单路udp转发方式.
|
||||||
|
|
||||||
|
此时从socks5读出的每一个udp链接,视其raddr而定,如果raddr是没遇到过的地址,则我们会新建立一个新信道
|
||||||
|
|
||||||
|
## 连接池
|
||||||
|
|
||||||
|
我们可以通过使用新协议命令来做到 非mux的 连接池技术,可以在每个vless 头部添加 “是否关闭连接”的信息
|
||||||
|
|
||||||
|
如果准备关闭连接,则实际上不关闭连接,而是标记这个连接进行备用,然后等客户端下一次想要创建一个新连接时,直接使用这个现有的连接。
|
||||||
|
|
||||||
|
与 多路mux的不同是,多路mux是有同时连接的上限的,而连接池技术则没有,所以还是会比多路mux的方法速度更快
|
||||||
|
|
||||||
|
|
||||||
|
## 服务端主动返回ip地址
|
||||||
|
|
||||||
|
我们访问vless时,是可以访问域名的,而此时客户端是 不知道该域名是否会被墙的;
|
||||||
|
|
||||||
|
如果有一种方法,可以让客户端知道该域名的真实ip(不能客户端自己查dns,因为可能被污染),那就好了。
|
||||||
|
|
||||||
|
如果我们在 vless v1的 服务端返回的首包的信息头中,头部首先返回该 目标的 实际 ip,这样就相当于客户端免费做了一次dns查询。
|
||||||
|
|
||||||
|
比如,头部 : 如果不返回ip,则第一字节为0;如果返回ip,则第一字节为1,然后第二字节为 ip类型,标识是v4还是v6,然后后面是实际ip数据,最后跟着的才是首包的实际数据
|
||||||
|
|
||||||
|
感觉这个方法不错,使用这种方法,则只需一个geoip数据库就可以判断 分流,不再需要额外的 geosite
|
||||||
|
|
||||||
|
|
||||||
|
## 内层加密
|
||||||
|
|
||||||
|
vmess的加密方式过于繁琐,而且如果怕封未知流量的话还是要套tls
|
||||||
|
|
||||||
|
有些小伙伴怕纯tls 容易被中间人攻击,那么我们如果内层也加密的话就不怕了
|
||||||
|
|
||||||
|
可以考虑推出一种简化版的加密方式. 当然显然,这个加密是可选的,而且默认不配置的话肯定我们是不加密的,因为vlessv1外面必套tls
|
||||||
|
|
||||||
|
vmess我觉得最复杂的地方就是客户端和服务端每次都要同时生成一大串数,很麻烦啊!
|
||||||
|
|
||||||
|
|
17
main.go
17
main.go
@@ -1326,7 +1326,22 @@ func dialClient_andRelay(iics incomingInserverConnState, targetAddr netLayer.Add
|
|||||||
|
|
||||||
atomic.AddInt32(&activeConnectionCount, 1)
|
atomic.AddInt32(&activeConnectionCount, 1)
|
||||||
|
|
||||||
netLayer.RelayUDP(udp_wrc, udp_wlc, &allDownloadBytesSinceStart, &allUploadBytesSinceStart)
|
if client.IsUDP_MultiChannel() {
|
||||||
|
utils.Debug("Relaying UDP with MultiChannel")
|
||||||
|
|
||||||
|
netLayer.RelayUDP_separate(udp_wrc, udp_wlc, &allDownloadBytesSinceStart, &allUploadBytesSinceStart, func(raddr netLayer.Addr) netLayer.MsgConn {
|
||||||
|
_, udp_wrc, _, _, result := dialClient(targetAddr, client, iics.baseLocalConn, nil, "", false)
|
||||||
|
if result == 0 {
|
||||||
|
return udp_wrc
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
netLayer.RelayUDP(udp_wrc, udp_wlc, &allDownloadBytesSinceStart, &allUploadBytesSinceStart)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
atomic.AddInt32(&activeConnectionCount, -1)
|
atomic.AddInt32(&activeConnectionCount, -1)
|
||||||
|
|
||||||
|
@@ -185,9 +185,9 @@ classic:
|
|||||||
return int64(n), e
|
return int64(n), e
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 wrc 读取 写入到 wlc ,并同时从 wlc 读取写入 wrc
|
// 从 wrc 读取 写入到 wlc ,并同时从 wlc 读取写入 wrc.
|
||||||
// 阻塞
|
// 阻塞.
|
||||||
// UseReadv==true 时 内部使用 TryCopy 进行拷贝
|
// UseReadv==true 时 内部使用 TryCopy 进行拷贝,
|
||||||
// 会自动优选 splice,readv,不行则使用经典拷贝.
|
// 会自动优选 splice,readv,不行则使用经典拷贝.
|
||||||
//
|
//
|
||||||
//拷贝完成后会主动关闭双方连接.
|
//拷贝完成后会主动关闭双方连接.
|
||||||
|
@@ -38,47 +38,46 @@ type MsgConn interface {
|
|||||||
Fullcone() bool //若Fullcone, 则在转发因另一端关闭而结束后, RelayUDP函数不会Close它.
|
Fullcone() bool //若Fullcone, 则在转发因另一端关闭而结束后, RelayUDP函数不会Close它.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//在转发时, 有可能有多种情况
|
||||||
|
/*
|
||||||
|
1. dokodemo 监听udp 定向 导向到 direct 的远程udp实际地址
|
||||||
|
此时因为是定向的, 所以肯定不是fullcone
|
||||||
|
|
||||||
|
dokodemo 用的是 UniTargetMsgConn, underlay 是 netLayer.UDPConn, 其已经设置了UDP_timeout
|
||||||
|
|
||||||
|
在 netLayer.UDPConn 超时后, ReadFrom 就会解放, 并触发双向Close, 来关闭我们的 direct的udp连接。
|
||||||
|
|
||||||
|
1.5. 比较少见的情况, dokodemo监听tcp, 然后发送到 direct 的udp. 此时客户应用程序可以手动关闭tcp连接来帮我们触发 udp连接 的 close
|
||||||
|
|
||||||
|
2. socks5监听 udp, 导向到 direct 的远程udp实际地址
|
||||||
|
|
||||||
|
socks5端只用一个udp连接来监听所有信息, 所以不能关闭, 所以没有设置超时
|
||||||
|
|
||||||
|
此时我们需要对 每一个 direct的udp连接 设置超时, 否则就会一直占用端口
|
||||||
|
|
||||||
|
3. socks5 监听udp, 导向到 trojan, 然后 服务端的 trojan 再导向 direct
|
||||||
|
|
||||||
|
trojan 也是用一个信道来接收udp的所有请求的, 所以trojan的连接也不能关.
|
||||||
|
|
||||||
|
所以依然需要在服务端 的 direct上面 加Read 时限
|
||||||
|
|
||||||
|
否则 rc.ReadFrom() 会卡住而不返回.
|
||||||
|
|
||||||
|
因为direct 使用 UDPMsgConnWrapper,而我们已经在 UDPMsgConnWrapper里加了这个逻辑, 所以可以放心了.
|
||||||
|
|
||||||
|
4. fullcone, 此时不能对整个监听端口进行close,会影响其它外部链接发来的连接。
|
||||||
|
|
||||||
|
5. vless v1 的 crumfurs 这种单路client的udp转发方式, 此时需要判断lc.ReadMsgFrom得到的 raddr是否是已知地址,
|
||||||
|
如果是未知的, 则不会再使用原来的rc,而是要拨号新通道
|
||||||
|
|
||||||
|
也就是说,lc是有且仅有一个的, 因为是socks5 或者dokodemo都是采用的单信道的方式,
|
||||||
|
|
||||||
|
而在 vless v1时, udp的rc的拨号可以采用多信道方式。
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
// 阻塞. 返回从 rc 下载的总字节数. 拷贝完成后自动关闭双端连接.
|
// 阻塞. 返回从 rc 下载的总字节数. 拷贝完成后自动关闭双端连接.
|
||||||
func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64 {
|
func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64 {
|
||||||
|
|
||||||
//在转发时, 有可能有多种情况
|
|
||||||
/*
|
|
||||||
1. dokodemo 监听udp 定向 导向到 direct 的远程udp实际地址
|
|
||||||
此时因为是定向的, 所以肯定不是fullcone
|
|
||||||
|
|
||||||
dokodemo 用的是 UniTargetMsgConn, underlay 是 netLayer.UDPConn, 其已经设置了UDP_timeout
|
|
||||||
|
|
||||||
在 netLayer.UDPConn 超时后, ReadFrom 就会解放, 并触发双向Close, 来关闭我们的 direct的udp连接。
|
|
||||||
|
|
||||||
1.5. 比较少见的情况, dokodemo监听tcp, 然后发送到 direct 的udp. 此时客户应用程序可以手动关闭tcp连接来帮我们触发 udp连接 的 close
|
|
||||||
|
|
||||||
2. socks5监听 udp, 导向到 direct 的远程udp实际地址
|
|
||||||
|
|
||||||
socks5端只用一个udp连接来监听所有信息, 所以不能关闭, 所以没有设置超时
|
|
||||||
|
|
||||||
此时我们需要对 每一个 direct的udp连接 设置超时, 否则就会一直占用端口
|
|
||||||
|
|
||||||
3. socks5 监听udp, 导向到 trojan, 然后 服务端的 trojan 再导向 direct
|
|
||||||
|
|
||||||
trojan 也是用一个信道来接收udp的所有请求的, 所以trojan的连接也不能关.
|
|
||||||
|
|
||||||
所以依然需要在服务端 的 direct上面 加Read 时限
|
|
||||||
|
|
||||||
否则 rc.ReadFrom() 会卡住而不返回.
|
|
||||||
|
|
||||||
因为direct 使用 UDPMsgConnWrapper,而我们已经在 UDPMsgConnWrapper里加了这个逻辑, 所以可以放心了.
|
|
||||||
|
|
||||||
4. fullcone, 此时不能对整个监听端口进行close,会影响其它外部链接发来的连接。
|
|
||||||
|
|
||||||
5. vless v1 的 crumfurs 这种单路client的udp转发方式, 此时需要判断lc.ReadMsgFrom得到的 raddr是否是已知地址,
|
|
||||||
如果是未知的, 则不会再使用原来的rc,而是要拨号新通道
|
|
||||||
|
|
||||||
也就是说,lc是有且仅有一个的, 因为是socks5 或者dokodemo都是采用的单信道的方式,
|
|
||||||
|
|
||||||
而在 vless v1时, udp的rc的拨号可以采用多信道方式。
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var count uint64
|
var count uint64
|
||||||
|
|
||||||
@@ -87,6 +86,7 @@ func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rc.WriteMsgTo(bs, raddr)
|
err = rc.WriteMsgTo(bs, raddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
@@ -139,6 +139,94 @@ func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relayUDP_rc_toLC(rc, lc MsgConn, downloadByteCount *uint64) uint64 {
|
||||||
|
var count uint64
|
||||||
|
for {
|
||||||
|
bs, raddr, err := rc.ReadMsgFrom()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = lc.WriteMsgTo(bs, raddr)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count += uint64(len(bs))
|
||||||
|
}
|
||||||
|
if !rc.Fullcone() {
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lc.Fullcone() {
|
||||||
|
lc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if downloadByteCount != nil {
|
||||||
|
atomic.AddUint64(downloadByteCount, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelayUDP_separate 对 lc 读到的每一个新raddr地址 都新拨号一次. 这样就避开了经典的udp多路复用转发的效率低下问题.
|
||||||
|
// 阻塞. 返回从 rc 下载的总字节数. 拷贝完成后自动关闭双端连接.
|
||||||
|
func RelayUDP_separate(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64, dialfunc func(raddr Addr) MsgConn) uint64 {
|
||||||
|
go func() {
|
||||||
|
var count uint64
|
||||||
|
|
||||||
|
rc_raddrMap := make(map[HashableAddr]MsgConn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
bs, raddr, err := lc.ReadMsgFrom()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
hash := raddr.GetHashable()
|
||||||
|
if len(rc_raddrMap) == 0 {
|
||||||
|
rc_raddrMap[hash] = rc
|
||||||
|
} else {
|
||||||
|
oldrc := rc_raddrMap[hash]
|
||||||
|
if oldrc != nil {
|
||||||
|
rc = oldrc
|
||||||
|
} else {
|
||||||
|
rc = dialfunc(raddr)
|
||||||
|
if rc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rc_raddrMap[hash] = rc
|
||||||
|
|
||||||
|
go relayUDP_rc_toLC(rc, lc, downloadByteCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = rc.WriteMsgTo(bs, raddr)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
count += uint64(len(bs))
|
||||||
|
}
|
||||||
|
if !rc.Fullcone() {
|
||||||
|
for _, thisrc := range rc_raddrMap {
|
||||||
|
thisrc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lc.Fullcone() {
|
||||||
|
lc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadByteCount != nil {
|
||||||
|
atomic.AddUint64(uploadByteCount, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
return relayUDP_rc_toLC(rc, lc, downloadByteCount)
|
||||||
|
}
|
||||||
|
|
||||||
// symmetric, proxy/dokodemo 有用到. 实现 MsgConn
|
// symmetric, proxy/dokodemo 有用到. 实现 MsgConn
|
||||||
type UniTargetMsgConn struct {
|
type UniTargetMsgConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
@@ -190,6 +278,8 @@ func NewUDPMsgConn(laddr *net.UDPAddr, fullcone bool, isserver bool) *UDPMsgConn
|
|||||||
uc := new(UDPMsgConn)
|
uc := new(UDPMsgConn)
|
||||||
|
|
||||||
udpConn, _ := net.ListenUDP("udp", laddr)
|
udpConn, _ := net.ListenUDP("udp", laddr)
|
||||||
|
udpConn.SetReadBuffer(MaxUDP_packetLen)
|
||||||
|
udpConn.SetWriteBuffer(MaxUDP_packetLen)
|
||||||
|
|
||||||
uc.conn = udpConn
|
uc.conn = udpConn
|
||||||
uc.fullcone = fullcone
|
uc.fullcone = fullcone
|
||||||
@@ -212,6 +302,7 @@ func (u *UDPMsgConn) ReadMsgFrom() ([]byte, Addr, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
n, ad, err := u.conn.ReadFromUDP(bs)
|
n, ad, err := u.conn.ReadFromUDP(bs)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Addr{}, err
|
return nil, Addr{}, err
|
||||||
}
|
}
|
||||||
@@ -230,6 +321,7 @@ func (u *UDPMsgConn) WriteMsgTo(bs []byte, raddr Addr) error {
|
|||||||
//非fullcone时, 强制 symmetric, 对每个远程地址 都使用一个 对应的新laddr
|
//非fullcone时, 强制 symmetric, 对每个远程地址 都使用一个 对应的新laddr
|
||||||
|
|
||||||
thishash := raddr.GetHashable()
|
thishash := raddr.GetHashable()
|
||||||
|
thishash.Network = "udp" //有可能调用者忘配置Network项了.
|
||||||
|
|
||||||
if len(u.symmetricMap) == 0 {
|
if len(u.symmetricMap) == 0 {
|
||||||
|
|
||||||
|
@@ -1,13 +1,11 @@
|
|||||||
package vless
|
package vless
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||||
@@ -22,18 +20,11 @@ func init() {
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
proxy.ProxyCommonStruct
|
proxy.ProxyCommonStruct
|
||||||
|
|
||||||
udpResponseChan chan netLayer.UDPAddrData
|
|
||||||
|
|
||||||
version int
|
version int
|
||||||
|
|
||||||
user *proxy.V2rayUser
|
user *proxy.V2rayUser
|
||||||
|
|
||||||
//is_CRUMFURS_established bool
|
udp_multi bool
|
||||||
|
|
||||||
mutex sync.RWMutex
|
|
||||||
knownUDPDestinations map[string]io.ReadWriter
|
|
||||||
|
|
||||||
crumfursBuf *bufio.Reader //在不使用ws或者grpc时,需要一个缓存 来读取 CRUMFURS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientCreator struct{}
|
type ClientCreator struct{}
|
||||||
@@ -54,9 +45,6 @@ func (_ ClientCreator) NewClient(dc *proxy.DialConf) (proxy.Client, error) {
|
|||||||
user: id,
|
user: id,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.knownUDPDestinations = make(map[string]io.ReadWriter)
|
|
||||||
c.udpResponseChan = make(chan netLayer.UDPAddrData, 20)
|
|
||||||
|
|
||||||
v := dc.Version
|
v := dc.Version
|
||||||
if v >= 0 {
|
if v >= 0 {
|
||||||
|
|
||||||
@@ -79,9 +67,6 @@ func NewClientByURL(url *url.URL) (proxy.Client, error) {
|
|||||||
c := Client{
|
c := Client{
|
||||||
user: id,
|
user: id,
|
||||||
}
|
}
|
||||||
c.knownUDPDestinations = make(map[string]io.ReadWriter)
|
|
||||||
c.udpResponseChan = make(chan netLayer.UDPAddrData, 20)
|
|
||||||
|
|
||||||
vStr := url.Query().Get("version")
|
vStr := url.Query().Get("version")
|
||||||
if vStr != "" {
|
if vStr != "" {
|
||||||
v, err := strconv.Atoi(vStr)
|
v, err := strconv.Atoi(vStr)
|
||||||
@@ -95,12 +80,22 @@ func NewClientByURL(url *url.URL) (proxy.Client, error) {
|
|||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Name() string { return Name }
|
func (c *Client) Name() string {
|
||||||
|
if c.version == 0 {
|
||||||
|
return Name
|
||||||
|
}
|
||||||
|
return Name + "_" + strconv.Itoa(c.version)
|
||||||
|
}
|
||||||
func (c *Client) Version() int { return c.version }
|
func (c *Client) Version() int { return c.version }
|
||||||
func (c *Client) GetUser() proxy.User {
|
func (c *Client) GetUser() proxy.User {
|
||||||
return c.user
|
return c.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) IsUDP_MultiChannel() bool {
|
||||||
|
|
||||||
|
return c.udp_multi
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Handshake(underlay net.Conn, target netLayer.Addr) (io.ReadWriteCloser, error) {
|
func (c *Client) Handshake(underlay net.Conn, target netLayer.Addr) (io.ReadWriteCloser, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -151,17 +146,23 @@ func (c *Client) EstablishUDPChannel(underlay net.Conn, target netLayer.Addr) (n
|
|||||||
|
|
||||||
return &UDPConn{Conn: underlay, version: c.version, isClientEnd: true, raddr: target}, err
|
return &UDPConn{Conn: underlay, version: c.version, isClientEnd: true, raddr: target}, err
|
||||||
|
|
||||||
//在vless v1中, 不使用 单独udp信道来传输所有raddr方向的数据
|
|
||||||
// 所以在v1中,我们不应用 EstablishUDPChannel 函数
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getBufWithCmd(cmd byte) *bytes.Buffer {
|
func (c *Client) getBufWithCmd(cmd byte) *bytes.Buffer {
|
||||||
v := c.version
|
v := c.version
|
||||||
buf := utils.GetBuf()
|
buf := utils.GetBuf()
|
||||||
buf.WriteByte(byte(v)) //version
|
buf.WriteByte(byte(c.version)) //version
|
||||||
buf.Write(c.user[:])
|
buf.Write(c.user[:])
|
||||||
if v == 0 {
|
if v == 0 {
|
||||||
buf.WriteByte(0) //addon length
|
buf.WriteByte(0) //addon length
|
||||||
|
} else {
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
buf.WriteByte(0) //no addon
|
||||||
|
case c.udp_multi:
|
||||||
|
buf.WriteByte(addon_udp_multi_flag)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
buf.WriteByte(cmd) // cmd
|
buf.WriteByte(cmd) // cmd
|
||||||
return buf
|
return buf
|
||||||
|
@@ -161,6 +161,7 @@ func (s *Server) Handshake(underlay net.Conn) (result io.ReadWriteCloser, msgCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
readbuf := bytes.NewBuffer(readbs[:wholeReadLen])
|
readbuf := bytes.NewBuffer(readbs[:wholeReadLen])
|
||||||
|
var use_udp_multi bool
|
||||||
|
|
||||||
goto realPart
|
goto realPart
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ realPart:
|
|||||||
|
|
||||||
s.mux4Hashes.RLock()
|
s.mux4Hashes.RLock()
|
||||||
|
|
||||||
thisUUIDBytes := *(*[16]byte)(unsafe.Pointer(&idBytes[0])) //下面crumfurs也有用到
|
thisUUIDBytes := *(*[16]byte)(unsafe.Pointer(&idBytes[0]))
|
||||||
|
|
||||||
if user := s.userHashes[thisUUIDBytes]; user != nil {
|
if user := s.userHashes[thisUUIDBytes]; user != nil {
|
||||||
s.mux4Hashes.RUnlock()
|
s.mux4Hashes.RUnlock()
|
||||||
@@ -205,7 +206,7 @@ realPart:
|
|||||||
|
|
||||||
addonLenByte, err := readbuf.ReadByte()
|
addonLenByte, err := readbuf.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
returnErr = err //凡是和的层Read相关的错误,一律不再返回Fallback信息,因为连接已然不可用
|
returnErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if addonLenByte != 0 {
|
if addonLenByte != 0 {
|
||||||
@@ -220,6 +221,18 @@ realPart:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
addonFlagByte, err := readbuf.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
returnErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch addonFlagByte {
|
||||||
|
case addon_udp_multi_flag:
|
||||||
|
use_udp_multi = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commandByte, err := readbuf.ReadByte()
|
commandByte, err := readbuf.ReadByte()
|
||||||
@@ -270,6 +283,7 @@ realPart:
|
|||||||
raddr: targetAddr,
|
raddr: targetAddr,
|
||||||
optionalReader: io.MultiReader(readbuf, underlay),
|
optionalReader: io.MultiReader(readbuf, underlay),
|
||||||
remainFirstBufLen: readbuf.Len(),
|
remainFirstBufLen: readbuf.Len(),
|
||||||
|
udp_multi: use_udp_multi,
|
||||||
}, targetAddr, nil
|
}, targetAddr, nil
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@@ -156,34 +156,6 @@ func (uc *UserTCPConn) Write(p []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
|
||||||
if uc.isUDP && !uc.hasAdvancedLayer {
|
|
||||||
|
|
||||||
// 这里暂时认为包裹它的连接是 tcp或者tls,而不是udp,如果udp的话,就不需要考虑粘包问题了,比如socks5的实现
|
|
||||||
// 我们目前认为只有tls是最防墙的,而且 魔改tls是有毒的,所以反推过来,这里udp就必须加长度头。
|
|
||||||
|
|
||||||
// 目前是这个样子。之后verysimple实现了websocket和grpc后,会添加判断,如果连接是websocket或者grpc连接,则不再加长度头
|
|
||||||
|
|
||||||
// tls和tcp都是基于流的,可以分开写两次,不需要buf存在;如果连接是websocket或者grpc的话,直接传输。
|
|
||||||
|
|
||||||
l := int16(len(p))
|
|
||||||
var lenBytes []byte
|
|
||||||
|
|
||||||
if l <= 255 {
|
|
||||||
lenBytes = []byte{0, byte(l)}
|
|
||||||
}
|
|
||||||
|
|
||||||
lenBytes = []byte{byte(l >> 8), byte(l << 8 >> 8)}
|
|
||||||
|
|
||||||
_, err := uc.Conn.Write(lenBytes)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return uc.Conn.Write(p)
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return uc.Conn.Write(p)
|
return uc.Conn.Write(p)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -226,21 +198,6 @@ func (uc *UserTCPConn) Read(p []byte) (int, error) {
|
|||||||
return from.Read(p)
|
return from.Read(p)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
|
||||||
if uc.isUDP && !uc.hasAdvancedLayer {
|
|
||||||
|
|
||||||
if len(uc.udpUnreadPart) > 0 {
|
|
||||||
copiedN := copy(p, uc.udpUnreadPart)
|
|
||||||
if copiedN < len(uc.udpUnreadPart) {
|
|
||||||
uc.udpUnreadPart = uc.udpUnreadPart[copiedN:]
|
|
||||||
} else {
|
|
||||||
uc.udpUnreadPart = nil
|
|
||||||
}
|
|
||||||
return copiedN, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return uc.readudp_withLenthHead(p)
|
|
||||||
}*/
|
|
||||||
return from.Read(p)
|
return from.Read(p)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package vless
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ type UDPConn struct {
|
|||||||
remainFirstBufLen int
|
remainFirstBufLen int
|
||||||
|
|
||||||
version int
|
version int
|
||||||
|
udp_multi bool
|
||||||
isClientEnd bool
|
isClientEnd bool
|
||||||
|
|
||||||
bufr *bufio.Reader
|
bufr *bufio.Reader
|
||||||
@@ -32,10 +34,11 @@ func (u *UDPConn) Fullcone() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UDPConn) WriteMsgTo(p []byte, raddr netLayer.Addr) error {
|
func (u *UDPConn) WriteMsgTo(p []byte, raddr netLayer.Addr) error {
|
||||||
|
writeBuf := utils.GetBuf()
|
||||||
|
defer utils.PutBuf(writeBuf)
|
||||||
|
|
||||||
//v0很垃圾,不支持fullcone,而是无视raddr,始终向最开始的raddr发送。
|
//v0很垃圾,不支持fullcone,而是无视raddr,始终向最开始的raddr发送。
|
||||||
if u.version == 0 {
|
if u.version == 0 {
|
||||||
writeBuf := utils.GetBuf()
|
|
||||||
|
|
||||||
if !u.isClientEnd && !u.notFirst {
|
if !u.isClientEnd && !u.notFirst {
|
||||||
u.notFirst = true
|
u.notFirst = true
|
||||||
@@ -47,38 +50,80 @@ func (u *UDPConn) WriteMsgTo(p []byte, raddr netLayer.Addr) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l := int16(len(p))
|
return u.writeDataTo(writeBuf, p)
|
||||||
|
|
||||||
writeBuf.WriteByte(byte(l >> 8))
|
} else { // v1
|
||||||
writeBuf.WriteByte(byte(l << 8 >> 8))
|
|
||||||
|
|
||||||
writeBuf.Write(p)
|
if u.udp_multi {
|
||||||
|
if u.isClientEnd {
|
||||||
|
//如果是客户端,则不存在raddr与初始地址不同的情况,因为这种情况已经在 netLayer.RelayUDP_separate 里过滤掉了
|
||||||
|
|
||||||
_, err := u.Conn.Write(writeBuf.Bytes()) //“直接return这个的长度” 是错的,因为写入长度只能小于等于len(p)
|
return u.writeDataTo(writeBuf, p)
|
||||||
|
|
||||||
utils.PutBuf(writeBuf)
|
} else {
|
||||||
|
//判断raddr是否与 u.raddr相同, 如果不相同, 则要传输umfurs信息
|
||||||
|
// umfurs信息将会提示客户端 下一次发送到此地址时,拨号一个新的 udp信道.
|
||||||
|
|
||||||
if err != nil {
|
if u.raddr.GetHashable() == raddr.GetHashable() {
|
||||||
return err
|
writeBuf.WriteByte(0)
|
||||||
}
|
return u.writeDataTo(writeBuf, p)
|
||||||
return nil
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if !u.isClientEnd {
|
writeBuf.WriteByte(1)
|
||||||
//判断raddr是否与 u.raddr相同, 如果不相同, 则要传输crumfurs信息
|
WriteAddrTo(writeBuf, raddr)
|
||||||
// crumfurs信息将会提示客户端 去建立一个新的 udp信道.
|
return u.writeDataTo(writeBuf, p)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
WriteAddrTo(writeBuf, raddr)
|
||||||
|
return u.writeDataTo(writeBuf, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
//return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UDPConn) readData_with_len() ([]byte, error) {
|
||||||
|
|
||||||
|
b1, err := uc.bufr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b2, err := uc.bufr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
l := int(int16(b1)<<8 + int16(b2))
|
||||||
|
|
||||||
|
bs := utils.GetBytes(l)
|
||||||
|
n, err := io.ReadFull(uc.bufr, bs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bs[:n], nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UDPConn) writeDataTo(writeBuf *bytes.Buffer, p []byte) error {
|
||||||
|
writeBuf.WriteByte(byte(len(p) >> 8))
|
||||||
|
writeBuf.WriteByte(byte(len(p) << 8 >> 8))
|
||||||
|
_, err := u.Conn.Write(writeBuf.Bytes())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
_, err := u.Conn.Write(p)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UDPConn) ReadMsgFrom() ([]byte, netLayer.Addr, error) {
|
func (u *UDPConn) ReadMsgFrom() ([]byte, netLayer.Addr, error) {
|
||||||
|
|
||||||
var from io.Reader = u.Conn
|
var from io.Reader = u.Conn
|
||||||
if u.optionalReader != nil {
|
if u.optionalReader != nil {
|
||||||
from = u.optionalReader
|
from = u.optionalReader
|
||||||
@@ -109,34 +154,44 @@ func (u *UDPConn) ReadMsgFrom() ([]byte, netLayer.Addr, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bs, err := u.read_with_v0_Head()
|
bs, err := u.readData_with_len()
|
||||||
return bs, u.raddr, err
|
return bs, u.raddr, err
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
if u.bufr == nil {
|
||||||
|
u.bufr = bufio.NewReader(from)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.udp_multi {
|
||||||
|
if u.isClientEnd {
|
||||||
|
//判断是否是 umfurs信息
|
||||||
|
// umfurs信息将会提示客户端 下一次发送到此地址时,拨号一个新的 udp信道.
|
||||||
|
b1, err := u.bufr.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, netLayer.Addr{}, err
|
||||||
|
}
|
||||||
|
switch b1 {
|
||||||
|
default:
|
||||||
|
return nil, netLayer.Addr{}, utils.ErrInErr{ErrDesc: "udp_multi client read first byte unexpected", ErrDetail: utils.ErrInvalidData, Data: b1}
|
||||||
|
case 0:
|
||||||
|
bs, err := u.readData_with_len()
|
||||||
|
return bs, u.raddr, err
|
||||||
|
case 1:
|
||||||
|
raddr, err := GetAddrFrom(u.bufr)
|
||||||
|
bs, err := u.readData_with_len()
|
||||||
|
return bs, raddr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bs, err := u.readData_with_len()
|
||||||
|
return bs, u.raddr, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
raddr, err := GetAddrFrom(u.bufr)
|
||||||
|
bs, err := u.readData_with_len()
|
||||||
|
|
||||||
|
return bs, raddr, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, netLayer.Addr{}, nil
|
//return nil, netLayer.Addr{}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UDPConn) read_with_v0_Head() ([]byte, error) {
|
|
||||||
|
|
||||||
b1, err := uc.bufr.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b2, err := uc.bufr.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l := int(int16(b1)<<8 + int16(b2))
|
|
||||||
|
|
||||||
bs := utils.GetBytes(l)
|
|
||||||
n, err := io.ReadFull(uc.bufr, bs)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bs[:n], nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,8 @@ const (
|
|||||||
CRUMFURS_ESTABLISHED byte = 20
|
CRUMFURS_ESTABLISHED byte = 20
|
||||||
|
|
||||||
CRUMFURS_Established_Str = "CRUMFURS_Established"
|
CRUMFURS_Established_Str = "CRUMFURS_Established"
|
||||||
|
|
||||||
|
addon_udp_multi_flag = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// CMD types, for vless and vmess
|
// CMD types, for vless and vmess
|
||||||
@@ -117,6 +119,16 @@ func GetAddrFrom(buf utils.ByteReader) (addr netLayer.Addr, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//依照 vless 协议的格式 依次写入 地址的 port, 域名/ip 信息
|
||||||
|
func WriteAddrTo(writeBuf utils.ByteWriter, raddr netLayer.Addr) {
|
||||||
|
writeBuf.WriteByte(byte(raddr.Port >> 8))
|
||||||
|
writeBuf.WriteByte(byte(raddr.Port << 8 >> 8))
|
||||||
|
abs, atyp := raddr.AddressBytes()
|
||||||
|
writeBuf.WriteByte(atyp)
|
||||||
|
writeBuf.Write(abs)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//https://github.com/XTLS/Xray-core/discussions/716
|
//https://github.com/XTLS/Xray-core/discussions/716
|
||||||
func GenerateXrayShareURL(dialconf *proxy.DialConf) string {
|
func GenerateXrayShareURL(dialconf *proxy.DialConf) string {
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hahahrfool/v2ray_simple/netLayer"
|
"github.com/hahahrfool/v2ray_simple/netLayer"
|
||||||
"github.com/hahahrfool/v2ray_simple/proxy"
|
"github.com/hahahrfool/v2ray_simple/proxy"
|
||||||
@@ -118,13 +119,12 @@ func TestVLess0_udp(t *testing.T) {
|
|||||||
testVLessUDP(0, netLayer.RandPortStr(), t)
|
testVLessUDP(0, netLayer.RandPortStr(), t)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func TestVLess1_udp(t *testing.T) {
|
func TestVLess1_udp(t *testing.T) {
|
||||||
//testVLessUDP(1, "9738", t) //无法使用 testVLessUDP,见其注释
|
testVLessUDP(1, "9738", t) //无法使用 testVLessUDP,见其注释
|
||||||
//}
|
}
|
||||||
|
|
||||||
// 完整模拟整个 vless v0 的udp请求 过程,即 客户端连接代理服务器,代理服务器试图访问远程服务器,这里是使用的模拟的办法模拟出一个远程udp服务器;
|
// 完整模拟整个 vless 的udp请求 过程,即 客户端连接代理服务器,代理服务器试图访问远程服务器,这里是使用的模拟的办法模拟出一个远程udp服务器;
|
||||||
// 其他tcp测试因为比较简单,不需要第二步测试,而这里需要
|
// 其他tcp测试因为比较简单,不需要第二步测试,而这里需要
|
||||||
// 不过实测,这个test暂时只能使用v0版本,因为 v1版本具有 独特信道,不能直接使用下面代码。
|
|
||||||
func testVLessUDP(version int, port string, t *testing.T) {
|
func testVLessUDP(version int, port string, t *testing.T) {
|
||||||
url := "vless://a684455c-b14f-11ea-bf0d-42010aaa0003@127.0.0.1:" + port + "?version=" + strconv.Itoa(version)
|
url := "vless://a684455c-b14f-11ea-bf0d-42010aaa0003@127.0.0.1:" + port + "?version=" + strconv.Itoa(version)
|
||||||
fakeServerEndLocalServer, hase, errx := proxy.ServerFromURL(url)
|
fakeServerEndLocalServer, hase, errx := proxy.ServerFromURL(url)
|
||||||
@@ -189,6 +189,7 @@ func testVLessUDP(version int, port string, t *testing.T) {
|
|||||||
|
|
||||||
// 发送数据
|
// 发送数据
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
_, err = fakeRealUDPServerListener.WriteToUDP(replydata, remoteAddr)
|
_, err = fakeRealUDPServerListener.WriteToUDP(replydata, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("udp write back err:", err)
|
t.Log("udp write back err:", err)
|
||||||
@@ -266,7 +267,7 @@ func testVLessUDP(version int, port string, t *testing.T) {
|
|||||||
|
|
||||||
err = wrc.WriteMsgTo(bs, na)
|
err = wrc.WriteMsgTo(bs, na)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("failed to write to FakeUDPServer : %v", err)
|
t.Logf("failed wrc.WriteMsgTo : %v", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -274,21 +275,21 @@ func testVLessUDP(version int, port string, t *testing.T) {
|
|||||||
bs, _, err = wrc.ReadMsgFrom()
|
bs, _, err = wrc.ReadMsgFrom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("failed io.ReadFull(rc, hello[:]) : %v", err)
|
t.Logf("failed wrc.ReadMsgFrom : %v", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = wlc.WriteMsgTo(bs, raddr)
|
err = wlc.WriteMsgTo(bs, raddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("failed wlc.Write(hello[:]) : %v", err)
|
t.Logf("failed wlc.WriteMsgTo : %v", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 之后转发所有流量,不再特定限制数据
|
// 之后转发所有流量,不再特定限制数据
|
||||||
netLayer.RelayUDP(wlc, wrc, nil, nil)
|
netLayer.RelayUDP(wrc, wlc, nil, nil)
|
||||||
//t.Log("Copy End?!", )
|
//t.Log("Copy End?!")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -315,14 +316,14 @@ func testVLessUDP(version int, port string, t *testing.T) {
|
|||||||
|
|
||||||
t.Log("client write hello success")
|
t.Log("client write hello success")
|
||||||
|
|
||||||
bs, _, _ := wrc.ReadMsgFrom()
|
bs, _, err := wrc.ReadMsgFrom()
|
||||||
if !bytes.Equal(bs, replydata) {
|
if !bytes.Equal(bs, replydata) {
|
||||||
t.Log("!bytes.Equal(world[:], replydata) ", bs, replydata)
|
t.Log("!bytes.Equal(world[:], replydata) ", bs, replydata, err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
t.Log("读到正确reply!")
|
t.Log("读到正确reply!")
|
||||||
|
|
||||||
//再试图发送长信息,确保 vless v0 的实现没有问题
|
//再尝试 发送 长信息,确保 vless v0 的实现没有问题
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
longbs := make([]byte, 9*1024)
|
longbs := make([]byte, 9*1024)
|
||||||
|
20
test_test.go
20
test_test.go
@@ -49,19 +49,23 @@ func TestDNSLookup_CN(t *testing.T) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func TestUDP_dokodemo_vless(t *testing.T) {
|
func TestUDP_dokodemo_vless(t *testing.T) {
|
||||||
testUDP_dokodemo_protocol("vless", "tcp", t)
|
testUDP_dokodemo_protocol("vless", 0, "tcp", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDP_dokodemo_vless_v1(t *testing.T) {
|
||||||
|
testUDP_dokodemo_protocol("vless", 1, "tcp", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUDP_dokodemo_trojan(t *testing.T) {
|
func TestUDP_dokodemo_trojan(t *testing.T) {
|
||||||
testUDP_dokodemo_protocol("trojan", "tcp", t)
|
testUDP_dokodemo_protocol("trojan", 0, "tcp", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUDP_dokodemo_trojan_through_udp(t *testing.T) {
|
func TestUDP_dokodemo_trojan_through_udp(t *testing.T) {
|
||||||
testUDP_dokodemo_protocol("trojan", "udp", t)
|
testUDP_dokodemo_protocol("trojan", 0, "udp", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
//经实测,udp dokodemo->vless/trojan (tcp/udp)->udp direct 来请求dns 是毫无问题的。
|
//经实测,udp dokodemo->vless/trojan (tcp/udp)->udp direct 来请求dns 是毫无问题的。
|
||||||
func testUDP_dokodemo_protocol(protocol string, network string, t *testing.T) {
|
func testUDP_dokodemo_protocol(protocol string, version int, network string, t *testing.T) {
|
||||||
utils.LogLevel = utils.Log_debug
|
utils.LogLevel = utils.Log_debug
|
||||||
utils.InitLog()
|
utils.InitLog()
|
||||||
|
|
||||||
@@ -78,14 +82,14 @@ protocol = "%s"
|
|||||||
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = %s
|
port = %s
|
||||||
version = 0
|
version = %d
|
||||||
insecure = true
|
insecure = true
|
||||||
network = "%s"
|
network = "%s"
|
||||||
`
|
`
|
||||||
clientListenPort := netLayer.RandPortStr()
|
clientListenPort := netLayer.RandPortStr()
|
||||||
clientDialPort := netLayer.RandPortStr()
|
clientDialPort := netLayer.RandPortStr()
|
||||||
|
|
||||||
testClientConfStr := fmt.Sprintf(testClientConfFormatStr, clientListenPort, protocol, clientDialPort, network)
|
testClientConfStr := fmt.Sprintf(testClientConfFormatStr, clientListenPort, protocol, clientDialPort, version, network)
|
||||||
|
|
||||||
const testServerConfFormatStr = `
|
const testServerConfFormatStr = `
|
||||||
[[dial]]
|
[[dial]]
|
||||||
@@ -96,14 +100,14 @@ protocol = "%s"
|
|||||||
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = %s
|
port = %s
|
||||||
version = 0
|
version = %d
|
||||||
insecure = true
|
insecure = true
|
||||||
cert = "cert.pem"
|
cert = "cert.pem"
|
||||||
key = "cert.key"
|
key = "cert.key"
|
||||||
network = "%s"
|
network = "%s"
|
||||||
`
|
`
|
||||||
|
|
||||||
testServerConfStr := fmt.Sprintf(testServerConfFormatStr, protocol, clientDialPort, network)
|
testServerConfStr := fmt.Sprintf(testServerConfFormatStr, protocol, clientDialPort, version, network)
|
||||||
|
|
||||||
clientConf, err := LoadTomlConfStr(testClientConfStr)
|
clientConf, err := LoadTomlConfStr(testClientConfStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -23,6 +23,12 @@ type ByteReader interface {
|
|||||||
Read(p []byte) (n int, err error)
|
Read(p []byte) (n int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bytes.Buffer 实现了 ByteReader
|
||||||
|
type ByteWriter interface {
|
||||||
|
WriteByte(byte) error
|
||||||
|
Write(p []byte) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
func IsFlagGiven(name string) bool {
|
func IsFlagGiven(name string) bool {
|
||||||
found := false
|
found := false
|
||||||
flag.Visit(func(f *flag.Flag) {
|
flag.Visit(func(f *flag.Flag) {
|
||||||
|
Reference in New Issue
Block a user