Skip to content

DNS: Avoid passing domain to WriteTo func#6163

Merged
RPRX merged 3 commits into
XTLS:mainfrom
kirilllivanov:fix-wireguard
May 28, 2026
Merged

DNS: Avoid passing domain to WriteTo func#6163
RPRX merged 3 commits into
XTLS:mainfrom
kirilllivanov:fix-wireguard

Conversation

@kirilllivanov

Copy link
Copy Markdown
Contributor

Fix a WireGuard outbound UDP regression when the destination is still a domain name.

After the UDP FullCone fix in #5858, udpConnClient.WriteMultiBuffer may use b.UDP as the packet destination. If that destination contains a domain, RawNetAddr() returns nil and WriteTo fails with errors like:

app/proxyman/outbound: failed to process outbound traffic > proxy/wireguard: connection ends > write udp [::]:48524: invalid argument

Copilot AI review requested due to automatic review settings May 19, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a WireGuard outbound UDP regression where per-packet destinations may still be domain names (causing RawNetAddr() to be nil and WriteTo to fail), by resolving domain UDP destinations before writing.

Changes:

  • Refactors DNS lookup into a lookupIP helper and applies it to the initial outbound destination resolution.
  • Adds per-packet UDP destination domain resolution (with caching) in udpConnClient.WriteMultiBuffer.
  • Adds unit tests covering per-packet destination resolution, default destination resolution, and case-insensitive caching.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
proxy/wireguard/client.go Adds a DNS lookup helper and resolves/caches domain UDP destinations before WriteTo.
proxy/wireguard/client_test.go Adds tests verifying domain resolution behavior for UDP packet destinations and caching semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread proxy/wireguard/client.go
Comment thread proxy/wireguard/client.go
@Fangliding

Copy link
Copy Markdown
Member

自从为freedom添加这个缓存后我一直在想什么客户端会产生 writeTo domain 这种行为看起来极其诡异
如果这是你遇到的真实问题可以提供使用情况吗 否则我倾向不加

@kirilllivanov

Copy link
Copy Markdown
Contributor Author

This was reproduced on main (1bdb488, Xray 26.5.9) by opening http://example.com/ through a local SOCKS inbound. The test config keeps the same DNS/routing/WireGuard path as the original report, but uses a SOCKS inbound instead of VLESS/Reality so the request can be made with curl. Kernel TUN is used for this repro (noKernelTun: false), matching the Linux path that reports the nil UDP destination as invalid argument.

{
  "log": {
    "access": "none",
    "loglevel": "debug",
    "dnsLog": false
  },
  "dns": {
    "hosts": {
      "dns.cloudflare.internal": [
        "162.159.36.1",
        "162.159.46.1"
      ]
    },
    "servers": [
      {
        "address": "dns",
        "port": 5353,
        "skipFallback": true
      },
      {
        "address": "dns.cloudflare.internal",
        "domains": [
          "domain:example.com"
        ],
        "skipFallback": true
      },
      {
        "address": "localhost",
        "domains": [
          "full:dns"
        ],
        "skipFallback": true
      }
    ],
    "queryStrategy": "UseIPv4",
    "disableFallback": true,
    "disableFallbackIfMatch": true,
    "tag": "dns_inbound"
  },
  "routing": {
    "domainStrategy": "AsIs",
    "rules": [
      {
        "type": "field",
        "domain": [
          "full:dns.cloudflare.internal"
        ],
        "inboundTag": [
          "dns_inbound"
        ],
        "outboundTag": "warp"
      },
      {
        "type": "field",
        "domain": [
          "domain:example.com"
        ],
        "inboundTag": [
          "inbound-vless"
        ],
        "outboundTag": "warp"
      }
    ]
  },
  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 10808,
      "protocol": "socks",
      "settings": {
        "auth": "noauth",
        "udp": true
      },
      "tag": "inbound-vless",
      "sniffing": {
        "enabled": true,
        "destOverride": [
          "http",
          "tls",
          "quic"
        ]
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "settings": {
        "domainStrategy": "ForceIPv4"
      },
      "tag": "direct"
    },
    {
      "protocol": "wireguard",
      "settings": {
        "secretKey": "***",
        "address": [
          "172.16.0.2/32",
          "***/128"
        ],
        "noKernelTun": false,
        "mtu": 1280,
        "reserved": [
          *,
          *,
          *
        ],
        "peers": [
          {
            "endpoint": "engage.cloudflareclient.com:2408",
            "publicKey": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=",
            "keepAlive": 0,
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ]
          }
        ],
        "domainStrategy": "ForceIPv4"
      },
      "tag": "warp"
    }
  ]
}

Request:

curl -v --max-time 30 --socks5-hostname 127.0.0.1:10808 http://example.com/

Relevant log excerpt:

2026/05/19 21:02:11.466451 [Info] [1636493351] proxy/socks: TCP Connect request to tcp:example.com:80
2026/05/19 21:02:11.467631 [Info] [1636493351] app/dispatcher: sniffed domain: example.com
2026/05/19 21:02:11.467659 [Info] [1636493351] app/dispatcher: taking detour [warp] for [tcp:example.com:80]
2026/05/19 21:02:11.468771 [Warning] proxy/wireguard: Using kernel TUN.
2026/05/19 21:02:11.476739 [Debug] app/dns: domain example.com matches following rules: [custom:{type:Domain value:"example.com"}(DNS idx:1)]
2026/05/19 21:02:11.477112 [Debug] app/dns: domain example.com will use DNS in order: [UDP:DNS.CLOUDFLARE.INTERNAL:53]
2026/05/19 21:02:11.477136 [Info] app/dns: UDP:DNS.CLOUDFLARE.INTERNAL:53 querying DNS for: example.com.
2026/05/19 21:02:11.477173 [Debug] transport/internet/udp: dispatch request to: udp:dns.cloudflare.internal:53
2026/05/19 21:02:11.477175 [Info] transport/internet/udp: establishing new connection for udp:dns.cloudflare.internal:53
2026/05/19 21:02:11.477185 [Info] app/dispatcher: taking detour [warp] for [udp:dns.cloudflare.internal:53]
2026/05/19 21:02:11.477326 [Info] app/dns: returning 2 IP(s) for domain dns.cloudflare.internal -> [162.159.36.1 162.159.46.1]
2026/05/19 21:02:11.477422 [Info] app/proxyman/outbound: app/proxyman/outbound: failed to process outbound traffic > proxy/wireguard: connection ends > write udp [::]:47740: invalid argument

@Fangliding

Copy link
Copy Markdown
Member

似乎来源于xray dns模块内部一个自以为是的修复
我会把它抽掉 这样就没必要这么改了

@kirilllivanov

Copy link
Copy Markdown
Contributor Author

Updated the PR to use a smaller fix. After checking the DNS UDP path, the packet-level UDP destination override in the DNS nameserver turned out to be unnecessary here because the same destination is already passed to udp.Dispatcher.Dispatch. However, removing that override alone was not enough: WireGuard resolved the domain into a local addr variable, but kept the original domain in destination.Address. For UDP, udpConnClient later uses c.dest.RawNetAddr(), so the default destination could still remain a domain and produce write udp [::]:... invalid argument. I also rebuilt and tested the PR branch with the reported DNS/WireGuard routing setup. The previous invalid argument / nil UDP destination error no longer appears in either kernel TUN or noKernelTun mode.

@Fangliding

Copy link
Copy Markdown
Member

这样看起来就行了

@Fangliding Fangliding changed the title WireGuard outbound: resolve domain UDP destinations before WriteTo DNS: Avoid passing domain to WriteTo func May 20, 2026
@RPRX

RPRX commented May 24, 2026

Copy link
Copy Markdown
Member

@Fangliding 是因为 WG 不支持 UDP domain 吗?那好像不止这一处吧,QUIC sniffer 应该也会弄出来这种

@Fangliding

Copy link
Copy Markdown
Member

@Fangliding 是因为 WG 不支持 UDP domain 吗?那好像不止这一处吧,QUIC sniffer 应该也会弄出来这种

本来就不支持 这里面传的标准IP包 就不存在目标是一个domain的情况 domain都会被预解析 被sniffer造出来 udp domain 也会 只是遇到writeTo里是domain会错乱 我问这东西哪来的 然后发现是xray dns模块里被误加的

@RPRX

RPRX commented May 24, 2026

Copy link
Copy Markdown
Member

目前没有其它情况会导致 WriteTo domain 吗

@Fangliding

Copy link
Copy Markdown
Member

反正core里造不出来(更新之后) 不知道其他人会是什么情况 writeto doamin 是极其奇怪的 我还没听说过

@RPRX

RPRX commented May 24, 2026

Copy link
Copy Markdown
Member

理论上支持 FullCone 的代理协议都能被客户端构造出 UDP WriteTo domain 然后传给服务端,不过 Xray 的 freedom 支持它

@RPRX

RPRX commented May 24, 2026

Copy link
Copy Markdown
Member

但是 Freedom 从被代理的目标 UDP ReadFrom 后目前只有第一个包的 domain 解析出的 IP 会被换回 domain

如果 redirect 的话那个 InitUnchangedAddr 似乎是 redirect 后的,破坏了客户端的 cone,这两点可以修一下

当然如果出站是 WG 的话,因为不支持 UDP domain 所以从一开始就是炸的,似乎可以参考 freedom 修一下

@kirilllivanov

Copy link
Copy Markdown
Contributor Author

Test failure looks like a race/flake inside the Sudoku E2E test harness rather than something related to the WireGuard/DNS fix. The failing case is transport/internet/finalmask/sudoku, specifically TestSudokuE2ETemp/vless-reality/prefer_ascii, and it failed with an early EOF while the test client was waiting for the echoed TCP response. I could reproduce neither this exact failure nor any WireGuard/DNS failure locally.

Could you please rerun the test?

@RPRX

RPRX commented May 24, 2026

Copy link
Copy Markdown
Member

GitHub 上跑 test 偶尔炸一个很正常,没改平台特定代码的话另外两个过了就行

@kirilllivanov

Copy link
Copy Markdown
Contributor Author

但是 Freedom 从被代理的目标 UDP ReadFrom 后目前只有第一个包的 domain 解析出的 IP 会被换回 domain

如果 redirect 的话那个 InitUnchangedAddr 似乎是 redirect 后的,破坏了客户端的 cone,这两点可以修一下

当然如果出站是 WG 的话,因为不支持 UDP domain 所以从一开始就是炸的,似乎可以参考 freedom 修一下

I think these can be fixed in the outbound UDP handling.
For Freedom, the packet writer and reader can share a UDP destination mapping instead of only remembering the initial dial target. When the writer resolves a per-packet UDP domain to an IP, it should cache domain -> IP and also record the reverse mapping resolved IP:port -> original destination. Then, when the reader receives a UDP response from ReadFrom, it can restore the original domain destination for any mapped packet, not only for the first dial target.
This also fixes the redirect case. The current InitUnchangedAddr can represent the redirected destination rather than the original client-visible destination, so responses may be reported with the wrong address and break the client's cone behavior. Keeping the original destination separately avoids that.
For WireGuard, since WG only carries normal IP packets and cannot send a domain as a packet destination, any UDP packet-level domain should be resolved before calling RawNetAddr() / WriteTo. It can use the same idea as Freedom: resolve and cache domain -> IP, write to the resolved IP address, and keep a reverse mapping so responses can still be reported back as the original domain destination when appropriate.

@RPRX RPRX merged commit cb206dd into XTLS:main May 28, 2026
38 of 39 checks passed
@Meo597

Meo597 commented May 28, 2026

Copy link
Copy Markdown
Collaborator

似乎来源于xray dns模块内部一个自以为是的修复
我会把它抽掉 这样就没必要这么改了

dns 被我动了很多,还好这行不是我干的

nebulabox added a commit to nebulabox/Xray-core that referenced this pull request Jun 2, 2026
* commit '94ffd50060f1cfd5d7482ec90a23a92bdefdff68': (107 commits)
  Xray-core v26.6.1
  Burst observatory: Fix init check (XTLS#6221)
  DNS outbound: Replace "reject" with "return" (`rCode` is 0 by default) (XTLS#6214)
  uTLS: Update `ModernFingerprints` map and `OtherFingerprints` map (XTLS#6181)
  Realm finalmask: Fix client punch peers (XTLS#6213)
  GitHub Action CI: Add Go source file format check (XTLS#6090)
  Burst observatory: Fix time compare, cancel pending ping on instance close or new schedule started (XTLS#6106)
  Finalmask: Add mkcp-legacy (UDP) to replace mkcp-* and legacy header-* (XTLS#6201)
  Sudoku finalmask: Harden UDP ReadFrom() against invalid packets (XTLS#6185)
  XICMP finalmask: Refactor & Speed up; Add multi `ips` and `dgram` mode (client) (XTLS#6168)
  Salamander finalmask: Support `packetSize` (Gecko in Hysteria v2.9.2) (XTLS#6198)
  Finalmask: Add Realm (UDP hole punching in Hysteria v2.9.1) (XTLS#6137)
  README.md: Add flutter_vless to Xray Wrapper in Others (XTLS#6197)
  DNS: Avoid panic on domain too long (XTLS#6207)
  header-custom finalmask: Remove headerConnMode headerReadAddrAware interface (XTLS#6193)
  noise finalmask: Better `reset` (XTLS#6188)
  DNS: Avoid passing domain to WriteTo func (XTLS#6163)
  Bump golang.org/x/net from 0.54.0 to 0.55.0 (XTLS#6192)
  Socks5 server: More standard UDP ASSOCIATE (RFC 1928) (XTLS#6149)
  Hysteria server: `tls.WithNextProto("h3")` by default (XTLS#6186)
  ...

# Conflicts:
#	core/core.go
@LjhAUMEM

LjhAUMEM commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

应该没有解决根本问题,b.UDP 本身就允许域名,这部分的修复已经包含在了 6257 的重构里

#6287

BobJustFry pushed a commit to BobJustFry/xray-core that referenced this pull request Jun 7, 2026
XTLS#6163 (comment)

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
@bytecategory

Copy link
Copy Markdown
Contributor

正core里造不出来(更新之后) 不知道其他人会是什么情况 writeto doamin 是极其奇怪的 我还没听说过

doamin是什么

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants