由新的udp架构重新设计udp分离信道传输方式和vless v1协议并初步实现代码

This commit is contained in:
hahahrfool
2022-04-11 18:47:35 +08:00
parent 6dca31545b
commit c5ab5a201c
13 changed files with 590 additions and 345 deletions

View File

@@ -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部分依然不能删掉;
不过我在这里做一些变化, v1 的addon第一字节不再是 addon长度, 而是addon类型,之后可以通过addon类型来判断addon长度.
对比v0的 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字节然后是承载数据。
然而,这只实现了一半,只记录旧连接是不够的,因为需要把目标的地址也附带发送到客户端,不然的话,客户端是无法区分到底是谁发来的。
二,非首包的格式:
这就是udp和tcp的不同。tcp可以确保目标总是自己拨号的那个而udp则是可以任意人都向你发信息
剩余数据包中因为目标raddr已经确定所以不再有 经典传输中的 port、atype、addr部分。
总之还是要通过升级vless到1版本来实现
不过, 因为我们服务端发往 客户端的方向 需要传输 UMFURS 信息,所以我们需要一字节来指示本次数据包的意义
具体的话也不一定需要新cmd直接在 CmdUDP时使用不同的数据格式即可可以参考socks5和trojan的格式标准
客户端 发往 服务端方向 的 数据格式
比如trojan的 https://trojan-gfw.github.io/trojan/protocol
两字节数据包长度,然后跟着承载数据。
## udp fullcone的信息传输过程
服务端 发往 客户端方向 的 数据格式:
tcp代理是不需要fullcone的也不可能实现而因为udp的特殊性质可能需要fullcone
第一字节为意义指示,
先观察正常的tcp代理请求发送一个tcp请求链接到代理服务器然后直接就会使用这个连接传输双向的数据这是因为目标是单一的。
0标识 普通 来自原始raddr的数据包此时 第二字节和第三字节为数据长度,然后跟着数据;
而udpfullcone的代理请求是较为复杂的尽管客户端只是发送了到 一个特定的远程 udp地址的 数据请求但是因为要实现fullcone 代理服务器所传来的信息不一定仅仅是 客户端第一次请求的 那个远程地址 所传来的,而可能是新的其它 地址所传来的参考NAT打洞
然后,在客户端接到这个新地址所发来的数据后, 客户端可能还会接着向这个新地址发送数据,而这时,代理服务器 需要能够判别,这个新地址是不是之前接受过信息的地址,如果是的话,则依然使用之前 代理服务器所使用的 端口进行发送如果不是的话那说明这个是无效信息作为fullcone某个端口A 只能向自己 第一个发送信息的 目的地址,以及 所有 曾经向 A 发送过来信息 的 远程地址 发送信息)
1标识 UMFURS 数据, 此时,第 2、3字节 为 port。第4字节为 atype然后是变长的addr内容定义与tcp的握手的包头一致。接着是udp数据包长度的2字节然后是承载数据。
所以随着udp代理的请求的时间推移可发送信息的 远程地址会 呈增加的趋势也就是说建立了与多个远程udp地址的 p2p链接。
如果是xray那种使用mux的办法就是复用一个相同的tcp链接 去传输 多个连续的可以目标不同的 udp连接。
如果是我们要实现的新方法则不是mux即 每一次udp请求 都要产生一个 tcp连接然后进行handshake。
# 其他情况
## 传输在websocket之上的情况
看起来反倒比mux复杂但是因为这个是并发的所以虽然延迟可能高一些但是却提高了吞吐量网速能快一些适合一些视频流的情况
之前还讨论过 在使用ws时免除udp长度信息的计划
而且同样作为高级层grpc和quic因为传输的是流, 不满足包的特征,是无法免去长度信息的。
所以我们在vless v1版本中除了并发的udp fullcone实现也是要实现多路复用的这样兼顾游戏与视频
仔细思考, 免除udp长度信息主要是为了避免读取端 进行额外缓存, 减少读取端的拷贝
为了避免抄袭嫌疑我当然不会重新使用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然后后面直接接 该信息的承载内容。所以这个信息长度至少为87字节头部和至少一字节数据
服务端对 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我觉得最复杂的地方就是客户端和服务端每次都要同时生成一大串数很麻烦啊
但是, 一旦实施的话,实际上会增加 发送端的 buf, 发送端需要一次额外拷贝到buf中, 再整体一起写入 websocket, 所以实际上 如果 把 【发送端和接收端】 作为一个 整体 来考虑的话, 该计划并没有减少任何 内存拷贝, 而且还增加了代码复杂度
因此,讨论中的 免除udp长度信息的计划 不切实际,不予支持。
而且, v1的分离信道方式还是要先读取一字节, 所以还是要缓存, 所以完全没必要。

211
docs/vless_v1_discussion.md Normal file
View 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请求链接到代理服务器然后直接就会使用这个连接传输双向的数据这是因为目标是单一的。
而udpfullcone的代理请求是较为复杂的尽管客户端只是发送了到 一个特定的远程 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然后后面直接接 该信息的承载内容。所以这个信息长度至少为87字节头部和至少一字节数据
服务端对 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我觉得最复杂的地方就是客户端和服务端每次都要同时生成一大串数很麻烦啊

15
main.go
View File

@@ -1326,8 +1326,23 @@ func dialClient_andRelay(iics incomingInserverConnState, targetAddr netLayer.Add
atomic.AddInt32(&activeConnectionCount, 1)
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)
return

View File

@@ -185,9 +185,9 @@ classic:
return int64(n), e
}
// 从 wrc 读取 写入到 wlc ,并同时从 wlc 读取写入 wrc
// 阻塞
// UseReadv==true 时 内部使用 TryCopy 进行拷贝
// 从 wrc 读取 写入到 wlc ,并同时从 wlc 读取写入 wrc.
// 阻塞.
// UseReadv==true 时 内部使用 TryCopy 进行拷贝,
// 会自动优选 splicereadv不行则使用经典拷贝.
//
//拷贝完成后会主动关闭双方连接.

View File

@@ -38,9 +38,6 @@ type MsgConn interface {
Fullcone() bool //若Fullcone, 则在转发因另一端关闭而结束后, RelayUDP函数不会Close它.
}
// 阻塞. 返回从 rc 下载的总字节数. 拷贝完成后自动关闭双端连接.
func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64 {
//在转发时, 有可能有多种情况
/*
1. dokodemo 监听udp 定向 导向到 direct 的远程udp实际地址
@@ -79,6 +76,8 @@ func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64
*/
// 阻塞. 返回从 rc 下载的总字节数. 拷贝完成后自动关闭双端连接.
func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64 {
go func() {
var count uint64
@@ -87,6 +86,7 @@ func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64
if err != nil {
break
}
err = rc.WriteMsgTo(bs, raddr)
if err != nil {
@@ -139,6 +139,94 @@ func RelayUDP(rc, lc MsgConn, downloadByteCount, uploadByteCount *uint64) uint64
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
type UniTargetMsgConn struct {
net.Conn
@@ -190,6 +278,8 @@ func NewUDPMsgConn(laddr *net.UDPAddr, fullcone bool, isserver bool) *UDPMsgConn
uc := new(UDPMsgConn)
udpConn, _ := net.ListenUDP("udp", laddr)
udpConn.SetReadBuffer(MaxUDP_packetLen)
udpConn.SetWriteBuffer(MaxUDP_packetLen)
uc.conn = udpConn
uc.fullcone = fullcone
@@ -212,6 +302,7 @@ func (u *UDPMsgConn) ReadMsgFrom() ([]byte, Addr, error) {
}
n, ad, err := u.conn.ReadFromUDP(bs)
if err != nil {
return nil, Addr{}, err
}
@@ -230,6 +321,7 @@ func (u *UDPMsgConn) WriteMsgTo(bs []byte, raddr Addr) error {
//非fullcone时, 强制 symmetric, 对每个远程地址 都使用一个 对应的新laddr
thishash := raddr.GetHashable()
thishash.Network = "udp" //有可能调用者忘配置Network项了.
if len(u.symmetricMap) == 0 {

View File

@@ -1,13 +1,11 @@
package vless
import (
"bufio"
"bytes"
"io"
"net"
"net/url"
"strconv"
"sync"
"github.com/hahahrfool/v2ray_simple/netLayer"
"github.com/hahahrfool/v2ray_simple/proxy"
@@ -22,18 +20,11 @@ func init() {
type Client struct {
proxy.ProxyCommonStruct
udpResponseChan chan netLayer.UDPAddrData
version int
user *proxy.V2rayUser
//is_CRUMFURS_established bool
mutex sync.RWMutex
knownUDPDestinations map[string]io.ReadWriter
crumfursBuf *bufio.Reader //在不使用ws或者grpc时需要一个缓存 来读取 CRUMFURS
udp_multi bool
}
type ClientCreator struct{}
@@ -54,9 +45,6 @@ func (_ ClientCreator) NewClient(dc *proxy.DialConf) (proxy.Client, error) {
user: id,
}
c.knownUDPDestinations = make(map[string]io.ReadWriter)
c.udpResponseChan = make(chan netLayer.UDPAddrData, 20)
v := dc.Version
if v >= 0 {
@@ -79,9 +67,6 @@ func NewClientByURL(url *url.URL) (proxy.Client, error) {
c := Client{
user: id,
}
c.knownUDPDestinations = make(map[string]io.ReadWriter)
c.udpResponseChan = make(chan netLayer.UDPAddrData, 20)
vStr := url.Query().Get("version")
if vStr != "" {
v, err := strconv.Atoi(vStr)
@@ -95,12 +80,22 @@ func NewClientByURL(url *url.URL) (proxy.Client, error) {
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) GetUser() proxy.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) {
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
//在vless v1中, 不使用 单独udp信道来传输所有raddr方向的数据
// 所以在v1中我们不应用 EstablishUDPChannel 函数
}
func (c *Client) getBufWithCmd(cmd byte) *bytes.Buffer {
v := c.version
buf := utils.GetBuf()
buf.WriteByte(byte(v)) //version
buf.WriteByte(byte(c.version)) //version
buf.Write(c.user[:])
if v == 0 {
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
return buf

View File

@@ -161,6 +161,7 @@ func (s *Server) Handshake(underlay net.Conn) (result io.ReadWriteCloser, msgCon
}
readbuf := bytes.NewBuffer(readbs[:wholeReadLen])
var use_udp_multi bool
goto realPart
@@ -191,7 +192,7 @@ realPart:
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 {
s.mux4Hashes.RUnlock()
@@ -205,7 +206,7 @@ realPart:
addonLenByte, err := readbuf.ReadByte()
if err != nil {
returnErr = err //凡是和的层Read相关的错误一律不再返回Fallback信息因为连接已然不可用
returnErr = err
return
}
if addonLenByte != 0 {
@@ -220,6 +221,18 @@ realPart:
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()
@@ -270,6 +283,7 @@ realPart:
raddr: targetAddr,
optionalReader: io.MultiReader(readbuf, underlay),
remainFirstBufLen: readbuf.Len(),
udp_multi: use_udp_multi,
}, targetAddr, nil
} else {

View File

@@ -156,34 +156,6 @@ func (uc *UserTCPConn) Write(p []byte) (int, error) {
}
} 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)
}
@@ -226,21 +198,6 @@ func (uc *UserTCPConn) Read(p []byte) (int, error) {
return from.Read(p)
} 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)
}

View File

@@ -2,6 +2,7 @@ package vless
import (
"bufio"
"bytes"
"io"
"net"
@@ -16,6 +17,7 @@ type UDPConn struct {
remainFirstBufLen int
version int
udp_multi bool
isClientEnd bool
bufr *bufio.Reader
@@ -32,10 +34,11 @@ func (u *UDPConn) Fullcone() bool {
}
func (u *UDPConn) WriteMsgTo(p []byte, raddr netLayer.Addr) error {
writeBuf := utils.GetBuf()
defer utils.PutBuf(writeBuf)
//v0很垃圾不支持fullcone而是无视raddr始终向最开始的raddr发送。
if u.version == 0 {
writeBuf := utils.GetBuf()
if !u.isClientEnd && !u.notFirst {
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))
writeBuf.WriteByte(byte(l << 8 >> 8))
} else { // v1
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 u.raddr.GetHashable() == raddr.GetHashable() {
writeBuf.WriteByte(0)
return u.writeDataTo(writeBuf, p)
} else {
writeBuf.WriteByte(1)
WriteAddrTo(writeBuf, raddr)
return u.writeDataTo(writeBuf, p)
}
}
} else {
WriteAddrTo(writeBuf, raddr)
return u.writeDataTo(writeBuf, p)
}
}
//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
}
return nil
} else {
if !u.isClientEnd {
//判断raddr是否与 u.raddr相同, 如果不相同, 则要传输crumfurs信息
// crumfurs信息将会提示客户端 去建立一个新的 udp信道.
} else {
_, err := u.Conn.Write(p)
return err
}
}
return nil
}
func (u *UDPConn) ReadMsgFrom() ([]byte, netLayer.Addr, error) {
var from io.Reader = u.Conn
if u.optionalReader != nil {
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
} else {
}
return nil, netLayer.Addr{}, nil
if u.bufr == nil {
u.bufr = bufio.NewReader(from)
}
func (uc *UDPConn) read_with_v0_Head() ([]byte, error) {
b1, err := uc.bufr.ReadByte()
if u.udp_multi {
if u.isClientEnd {
//判断是否是 umfurs信息
// umfurs信息将会提示客户端 下一次发送到此地址时,拨号一个新的 udp信道.
b1, err := u.bufr.ReadByte()
if err != nil {
return nil, err
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
}
b2, err := uc.bufr.ReadByte()
if err != nil {
return nil, 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()
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, raddr, err
}
return bs[:n], nil
}
//return nil, netLayer.Addr{}, nil
}

View File

@@ -19,6 +19,8 @@ const (
CRUMFURS_ESTABLISHED byte = 20
CRUMFURS_Established_Str = "CRUMFURS_Established"
addon_udp_multi_flag = 1
)
// CMD types, for vless and vmess
@@ -117,6 +119,16 @@ func GetAddrFrom(buf utils.ByteReader) (addr netLayer.Addr, err error) {
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
func GenerateXrayShareURL(dialconf *proxy.DialConf) string {

View File

@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/hahahrfool/v2ray_simple/netLayer"
"github.com/hahahrfool/v2ray_simple/proxy"
@@ -118,13 +119,12 @@ func TestVLess0_udp(t *testing.T) {
testVLessUDP(0, netLayer.RandPortStr(), t)
}
//func TestVLess1_udp(t *testing.T) {
//testVLessUDP(1, "9738", t) //无法使用 testVLessUDP见其注释
//}
func TestVLess1_udp(t *testing.T) {
testVLessUDP(1, "9738", t) //无法使用 testVLessUDP见其注释
}
// 完整模拟整个 vless v0 的udp请求 过程,即 客户端连接代理服务器代理服务器试图访问远程服务器这里是使用的模拟的办法模拟出一个远程udp服务器
// 完整模拟整个 vless 的udp请求 过程,即 客户端连接代理服务器代理服务器试图访问远程服务器这里是使用的模拟的办法模拟出一个远程udp服务器
// 其他tcp测试因为比较简单不需要第二步测试而这里需要
// 不过实测这个test暂时只能使用v0版本因为 v1版本具有 独特信道,不能直接使用下面代码。
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)
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)
if err != nil {
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)
if err != nil {
t.Logf("failed to write to FakeUDPServer : %v", err)
t.Logf("failed wrc.WriteMsgTo : %v", err)
t.Fail()
return
}
@@ -274,21 +275,21 @@ func testVLessUDP(version int, port string, t *testing.T) {
bs, _, err = wrc.ReadMsgFrom()
if err != nil {
t.Logf("failed io.ReadFull(rc, hello[:]) : %v", err)
t.Logf("failed wrc.ReadMsgFrom : %v", err)
t.Fail()
return
}
err = wlc.WriteMsgTo(bs, raddr)
if err != nil {
t.Logf("failed wlc.Write(hello[:]) : %v", err)
t.Logf("failed wlc.WriteMsgTo : %v", err)
t.Fail()
return
}
// 之后转发所有流量,不再特定限制数据
netLayer.RelayUDP(wlc, wrc, nil, nil)
//t.Log("Copy End?!", )
netLayer.RelayUDP(wrc, wlc, nil, nil)
//t.Log("Copy End?!")
}()
}
}()
@@ -315,14 +316,14 @@ func testVLessUDP(version int, port string, t *testing.T) {
t.Log("client write hello success")
bs, _, _ := wrc.ReadMsgFrom()
bs, _, err := wrc.ReadMsgFrom()
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.Log("读到正确reply")
//再试图发送长信息,确保 vless v0 的实现没有问题
//再尝试 发送 长信息,确保 vless v0 的实现没有问题
for i := 0; i < 10; i++ {
longbs := make([]byte, 9*1024)

View File

@@ -49,19 +49,23 @@ func TestDNSLookup_CN(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) {
testUDP_dokodemo_protocol("trojan", "tcp", t)
testUDP_dokodemo_protocol("trojan", 0, "tcp", 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 是毫无问题的。
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.InitLog()
@@ -78,14 +82,14 @@ protocol = "%s"
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
host = "127.0.0.1"
port = %s
version = 0
version = %d
insecure = true
network = "%s"
`
clientListenPort := netLayer.RandPortStr()
clientDialPort := netLayer.RandPortStr()
testClientConfStr := fmt.Sprintf(testClientConfFormatStr, clientListenPort, protocol, clientDialPort, network)
testClientConfStr := fmt.Sprintf(testClientConfFormatStr, clientListenPort, protocol, clientDialPort, version, network)
const testServerConfFormatStr = `
[[dial]]
@@ -96,14 +100,14 @@ protocol = "%s"
uuid = "a684455c-b14f-11ea-bf0d-42010aaa0003"
host = "127.0.0.1"
port = %s
version = 0
version = %d
insecure = true
cert = "cert.pem"
key = "cert.key"
network = "%s"
`
testServerConfStr := fmt.Sprintf(testServerConfFormatStr, protocol, clientDialPort, network)
testServerConfStr := fmt.Sprintf(testServerConfFormatStr, protocol, clientDialPort, version, network)
clientConf, err := LoadTomlConfStr(testClientConfStr)
if err != nil {

View File

@@ -23,6 +23,12 @@ type ByteReader interface {
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 {
found := false
flag.Visit(func(f *flag.Flag) {