Xray-core
Xray-core copied to clipboard
FakeDNS 透明代理 UDP 回程数据包地址不匹配
问题描述
OpenWrt 上 Xray 作为透明代理,当 FakeDNS 生效时,发往域名的 UDP 数据可以正常发出送达,但是回程 UDP 数据没法收到,抓包显示回程包里远程UDP服务器的地址和去程不匹配。
关闭 FakeDNS,或者直接使用 IP 则 UDP 数据可以正常收发。
环境配置
UDP client --> Xray client on OpenWrt --(VLESS+REALITY)-> Xray server --> UDP server
UDP client: macOS 14.4.1 10.0.0.154 Xray client: v1.8.10 x86_64 OpenWrt 23.05.3; Passwall2 1.28-5 10.0.0.1 Xray server: v1.8.10 x86_64 Debian 12; Nginx 1.22.1 x.x.x.x UDP server: x86_64 Ubuntu 22.04 141.141.141.141
客户端配置(Passwall2 生成,只列出相关部分)
{
"dns": {
"disableFallback": true,
"queryStrategy": "UseIP",
"disableFallbackIfMatch": true,
"servers": [
...
{
"port": 15354,
"_flag": "China",
"address": "127.0.0.1",
"queryStrategy": "UseIP",
"domains": [
"geosite:cn"
]
},
{
"domains": [
"geosite:geolocation-!cn",
],
"_flag": "Proxy",
"address": "fakedns"
}
],
"disableCache": false,
"tag": "dns-in1"
},
"fakedns": [
{
"ipPool": "198.18.0.0\/16",
"poolSize": 65535
}
],
"outbounds": [
{
"_flag_tag": "...",
"_flag_proxy": 0,
"_flag_proxy_tag": "nil",
"settings": {
"vnext": [
{
"port": 443,
"users": [
{
"flow": "xtls-rprx-vision",
"encryption": "none",
"id": "d3d3b1....164a05d",
"level": 0
}
],
"address": "..."
}
]
},
"streamSettings": {
"network": "tcp",
"tcpSettings": {
"header": {
"type": "none"
}
},
"sockopt": {
"tcpFastOpen": true,
"mark": 255,
"tcpNoDelay": true
},
"realitySettings": {
"publicKey": "H...I",
"spiderX": "\/",
"serverName": "...",
"shortId": "d...6",
"fingerprint": "chrome"
},
"security": "reality"
},
"mux": {
"enabled": false
},
"protocol": "vless",
"tag": "..."
},
],
...
}
服务器端配置
{
...
"inbounds": [
{
"listen": "/dev/shm/reality.socket,0666",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "d...d",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none",
"fallbacks": [
{
"dest": 8004,
"xver": 1
}
]
},
"streamSettings": {
"network": "tcp",
"tcpSettings": {
"acceptProxyProtocol": true
},
"security": "reality",
"realitySettings": {
"show": false,
"dest": "/dev/shm/nginx.socket",
"xver": 1,
"serverNames": [
"vr.tcp.one"
],
"privateKey": "6...0",
"shortIds": [
"d...6"
]
},
"sockopt": {
"tcpMptcp": false,
"tcpNoDelay": true
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"domainsExcluded": [
"courier.push.apple.com"
]
}
},
...
}
重现步骤
最简化重现:实际应用是视频通讯软件,为方便分析只用 nc/ncat 重现,用一台远程机器 udp.abc.com (IP: 141.141.141.141) 模拟目标 UDP 服务器。
- 目标UDP服务器模拟应用服务器,打开 nc 侦听 9000 端口
- nc 客户端"连接"到目标服务器 9000 端口。
- nc 客户端输入 "Send 01" 模拟发出 UDP 包
- UDP服务器端,看到 "Send 01" 后输入 "Respond 01" 模拟回应
- 用序号 02 重复步骤3、4
nc客户端只能看到自己的 “Send" 消息,看不到 UDP 服务器回应的 "Respond" 消息。
❯ nc -uv udp.xxx.com 9000
Connection to udp.abc.com port 9000 [udp/cslistener] succeeded!
Send 01
Send 02
^C
UDP 服务器端能看到完整的消息
ubuntu@udp:~$ nc -ulv 9000
Bound on 0.0.0.0 9000
Connection received on ... 35301
XXXXSend 01
Respond 01
Send 02
Respond 02
Xray server 上日志看到发往域名
2024/03/30 18:15:43 tcp:x.x.x.x:6007 accepted udp:udp.abc.com:9000 [direct]
OpenWrt 上在 LAN (eth0) 抓包可以看到:
去程是客户端 10.0.0.154 发往 FakeDNS 地址 198.18.134.159
回程是 UDP服务器的真实地址 141.141.141.141 发往 10.0.0.154,并没有经过 FakeDNS 处理。
root@OpenWrt:~# tcpdump -nvv -i eth0 port 9000
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:15:42.835784 IP (tos 0x0, ttl 64, id 53177, offset 0, flags [none], proto UDP (17), length 29)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 1
18:15:42.835852 IP (tos 0x0, ttl 64, id 55321, offset 0, flags [none], proto UDP (17), length 29)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 1
18:15:42.835852 IP (tos 0x0, ttl 64, id 28711, offset 0, flags [none], proto UDP (17), length 29)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 1
18:15:42.835852 IP (tos 0x0, ttl 64, id 51857, offset 0, flags [none], proto UDP (17), length 29)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 1
18:16:21.118496 IP (tos 0x0, ttl 64, id 18397, offset 0, flags [none], proto UDP (17), length 36)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 8
18:17:27.548848 IP (tos 0x0, ttl 64, id 26499, offset 0, flags [none], proto UDP (17), length 36)
10.0.0.154.59439 > 198.18.134.159.9000: [udp sum ok] UDP, length 8
18:17:27.729226 IP (tos 0x0, ttl 64, id 30739, offset 0, flags [DF], proto UDP (17), length 39)
141.141.141.141.9000 > 10.0.0.154.59439: [udp sum ok] UDP, length 11
18:17:32.072717 IP (tos 0x0, ttl 64, id 30814, offset 0, flags [DF], proto UDP (17), length 40)
141.141.141.141.9000 > 10.0.0.154.59439: [udp sum ok] UDP, length 12
^C
8 packets captured
8 packets received by filter
0 packets dropped by kernel
相关参考
之前有类似症状的 issue #2962,但是环境和用例不同,并不确定是不是同一问题。
由于任何流量最终要发往实际的网络(IP 之间的通信),即使从 freedom 发出域名地址(由系统解析为 IP)的请求,响应也只会是 IP 地址,造成请求与响应的地址不匹配。
要么
1、给 outbound 加 domain_strategy
(我修改这个时有提过但是没有下文),强制把 UDP 的域名解析成 IP 再发出(like Clash)
2、freedom 做个 域名-IP 的映射把 UDP 响应中的 IP 替换成域名,但造成其他潜在问题,且大多数代理软件服务端不支持(like sing-box with udp_disable_domain_unmapping
disabled)
3、对域名地址的 UDP 回退到从前没有 NAT 行为可言的处理方式
之前有类似症状的 issue #2962 ,但是环境和用例不同,并不确定是不是同一问题。
测试过的确如此
3、对域名地址的 UDP 回退到从前没有 NAT 行为可言的处理方式
当然如果做成全部 symmetric NAT 也许不是问题(这个是 XUDP 实现前的临时解决方案),不过这样的话就缺失了 XUDP 的可 cone NAT 特性。
1、给 outbound 加
domain_strategy
这个相当于以前的行为(客户端强制对 UDP 地址进行解析再发送到服务端)的可控制版,但是有一定几率会出现客户端错误配置(这个情况应该很少,比如服务端为单栈服务器但是客户端设置为了相反的情况)导致的代理 UDP 连接问题。(印象中 v2fly 那边引入强行解析域名的 domain_strategy
时也讨论过这个)
2、freedom 做个 域名-IP 的映射把 UDP 响应中的 IP 替换成域名
这个的确属于最复杂的,而且会引入和其它代理工具交互的兼容性问题。不过如果不考虑兼容性并且假定客户端和服务端都能很快更新到最新的话能够做到这个特性还是很有吸引力的。
如果这样做的话,不一定是 freedom,任何在到达下一个端点时必须强制解析的情况应该也可以这样做(当然是个设想,可能不太完善):
- 服务端的 freedom UDP 端口分配或者下一端点 UDP 流 ID 和 客户端后的应用的 UDP 端口 映射关系保持一致,这样可以避免后续的问题。(如果 XUDP 流 ID 的分配和客户端后的应用的端口是一致的应该没问题) ~~2. domain-IP(-port) 映射表应该各自保存在 freedom 中开出的各个 UDP 端口或者转发到下一端点的各个 UDP 流 ID 上,不要全局使用同一个映射表。~~ ~~3. 如果是 symmetric NAT 映射的话,映射表为 domain-IP-port;如果是 cone NAT 映射的话,映射表为 domain-IP。~~
- domain-IP-port 映射表应该各自保存在 freedom 中开出的各个 UDP 端口或者转发到下一端点的各个 UDP 映射 ID 上,不要全局使用同一个映射表,或者至少这个全局映射表再保存一个对应的 freedom UDP 端口或者映射 ID 避免不同的客户端映射串台。
- 如果是 symmetric NAT 映射的话,回包地址做 domain-IP-port 严格对应;如果是 cone NAT 映射的话,已知端口回包地址仍旧做 domain-IP-port 对应即可,而来自同一地址的不同端口仅需对应 domain-IP。
- 这个应该是个比较麻烦的:非 symmetric NAT 的情况,多个域名指向同一个 IP,且客户端应用还是用同一个端口对这些域名发起访问:已知端口的回包做 domain-IP-port 对应,来自同一 IP 的未知的端口回包按照最新的 domain-IP 对应。但是具体效果要做大面积测试才知道。
- 如果涉及映射记录如何老化的话,~~可能用定时老化或者由客户端进行控制,比如传输映射超时信息或者由客户端发起老化提示~~
我这里测试也是同样的效果 不使用fakedns则正常
任何在到达下一个端点时必须强制解析的情况应该也可以这样做
这么说的话,freedom 的 redirect、freedom 的 domainStrategy 等,可能一直以来都有此问题。
当然是个设想,可能不太完善
是否想得太复杂,简单替换一下地址应付一下就算了https://github.com/SagerNet/sing/blob/3f6c423e76f0c33bcb926b0d69168734951db23f/common/bufio/nat.go#L82-L95
强制把 UDP 的域名解析成 IP 再发出
只能在客户端上这样做,如果中转、服务端这样做就会造成客户端出现此 issue 所述的问题了。不过这个不应该考虑,也没有人考虑。
简单替换一下地址应付一下就算了
大部分情况应该没有问题,只要这个映射包含从服务端哪个端口发出去的信息避免回包时映射串台就行。(比如通过代理访问一个开了 HTTP/3 的同时跑多个网站的多个域名指向同一个 IP 的服务器)
大部分情况应该没有问题,只要这个映射包含从服务端哪个端口发出去的信息避免回包时映射串台就行。(比如通过代理访问一个开了 HTTP/3 的同时跑多个网站的多个域名指向同一个 IP 的服务器)
这种一般不会使用同一个本地地址。如果复用本地地址,现在的屎山架构只能根据第一个目标地址进行(#1011 或许可以解决 fake DNS,但对于其他问题没有帮助)。 由于 UDP 不是面向连接的,且 IP 与域名之间不是“一一映射”,无法真正解决 UDP 地址替换的问题。
这种一般不会使用同一个本地地址。
明白了。
由于 UDP 不是面向连接的,且 IP 与域名之间不是“一一映射”,无法真正解决 UDP 地址替换的问题。
同意,要几乎解决 UDP 在代理里面的地址问题真的比较挠头。就先按照简单的做出来看看效果?
~~还好 1.9.0 的计划里面没包含 UDP 优化相关内容不然 release blocker #1967~~
Fake DNS这种侵入性的解决方案(或者难听点叫歪门邪道)不可避免的问题 谷歌上搜这个词 最前面还是两家ray的文档 除了继续堆史山加入一些侵入性更强的特性基本没法解决问题 我决定这不算核心bug(