Skip to content

feat(xdns): multi-resolver fan-out + fix(kcp): reduce aggressive retransmissions#5872

Open
nnemirovsky wants to merge 16 commits intoXTLS:mainfrom
nnemirovsky:xdns-resolver-multiplexing
Open

feat(xdns): multi-resolver fan-out + fix(kcp): reduce aggressive retransmissions#5872
nnemirovsky wants to merge 16 commits intoXTLS:mainfrom
nnemirovsky:xdns-resolver-multiplexing

Conversation

@nnemirovsky
Copy link
Copy Markdown

Cleaned up resubmission of #5871. Removed unnecessary files, squashed to 2 commits.

Changes

1. XDNS multi-resolver fan-out

Optional resolvers config field. When set, the client distributes DNS queries across multiple public resolvers within a single mKCP session for higher throughput.

"finalmask": {
  "udp": [{"type": "xdns", "settings": {"domain": "t.example.com", "resolvers": ["1.1.1.1", "8.8.8.8"]}}]
}
  • One UDP socket per resolver with independent receive goroutines
  • Round-robin distribution
  • Backward compatible (omitting resolvers = direct mode)
  • Fix server sendLoop: drain stale queries, reduce response delay from 1s to 50ms, increase write queue from 512 to 4096

2. mKCP: respect congestion control (per RPRX suggestion in #5871)

The cwnd *= 20 multiplier allowed 20x more in-flight packets than the congestion window, defeating congestion control. When congestion: true, the multiplier is now skipped so mKCP doesn't flood low-bandwidth transports like XDNS.

Connection timeout increased from 30s to 120s to accommodate DNS tunnel latency.

Test plan

  • TestParseResolverAddr: resolver address parsing
  • TestResolverModeRoundTrip: mock resolver end-to-end
  • TestMultiResolverDistribution: round-robin verification
  • TestDirectModeRoundTrip / TestResolverModeServerToClient: bidirectional data
  • HTTPS verified working through XDNS on localhost

Add optional `resolvers` config field to XDNS finalmask. When set,
the client sends DNS queries through public DNS resolvers instead of
connecting directly to the server on port 53.

- One UDP socket per resolver with independent receive goroutines
- Round-robin query distribution across resolvers
- Backward compatible: omitting resolvers preserves direct mode
- Fix server sendLoop starvation under mKCP retransmission flood
- Drain excess query records to skip stale queries
- Reduce server response delay from 1s to 50ms
- Increase server write queue from 512 to 4096
…s enabled

The cwnd *= 20 multiplier allowed 20x more packets in flight than the
congestion window, defeating the purpose of congestion control. When
congestion is enabled, respect the actual cwnd without the multiplier.
This prevents mKCP from flooding low-bandwidth transports like XDNS.

Also increase connection timeout from 30s to 120s to accommodate
high-latency transports like DNS tunneling.
@LjhAUMEM
Copy link
Copy Markdown
Collaborator

LjhAUMEM commented Mar 31, 2026

终于知道为什么反感 AI 了

粗略看了下,我设计 xdns 里的 closed 不需要 sync,不存在竞态,xdns 的服务端天然支持从不同 dns 收发数据,理论无需改动,多 conn 收发也只需改动客户端

怎么说呢,我能理解你的想法,但放在 mask 里并不合适,mask 不应该进行额外的 dial,目前是允许 xdns 在任意层的,虽然不在最后一层没有什么用

如果要接受这个 pr,要么把 xdns 剥离到独立传输层,要么加上限制 xdns 和 xicmp 一样只能处于最外层,且不能搭配 udphop 与 dialerproxy

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

或许可以利用 writeto 的 addr 特性来实现 multi dns,如果能不新建 conn,那么可以留在 mask

Address LjhAUMEM review: masks should not create new connections.
Use WriteTo addr on the existing PacketConn to send to different
resolvers instead of creating separate sockets. Revert server
changes (server already supports data from different DNS sources).
Remove sockopt files, sync changes, and layer validation.
@RPRX
Copy link
Copy Markdown
Member

RPRX commented Mar 31, 2026

或许可以利用 writeto 的 addr 特性来实现 multi dns,如果能不新建 conn,那么可以留在 mask

结果被运营商按源端口限速

XDNS 可以限制为必须在最外层,也就 noise 有点用,udphop 对它来说没啥用,dialerproxy 实在不行可以利用 tun 来实现

话说我一直想把出站的 address port 完全移到传输层来着,可能移了的话对这种情况来说更方便

没看代码,这个特性就简单些用数组而不是 map 吧,随机选择,重复次数多的 ip 更容易被选中

@RPRX
Copy link
Copy Markdown
Member

RPRX commented Mar 31, 2026

@LjhAUMEM 或许改一下 noise 让它兼容一下 XDNS 在倒数第二层的情况

@RPRX
Copy link
Copy Markdown
Member

RPRX commented Mar 31, 2026

但公共 UDP 明文 DNS 基本上都是 53 端口而且这时候 DNS 又不需要握手,可能审查者就直接按 DNS 按单个包分析流量了

所以 noise 对 XDNS 有没有用我也不清楚

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

@LjhAUMEM 或许改一下 noise 让它兼容一下 XDNS 在倒数第二层的情况

可以搞成忽略前面所有的 noise

话说我想等串流那个解决了再来审这个

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

不过这个最好搭配个 mux,kcp 客户端没有 mux 单条连接其实不会持续很久,客户端日志的大量 xdns closed 能证明这一点,再加上 multi dns,监听的端口几倍增长

@RPRX
Copy link
Copy Markdown
Member

RPRX commented Mar 31, 2026

话说我想等串流那个解决了再来审这个

那个我感觉可能是 DispatchLink() 的问题

不过这个最好搭配个 mux

XDRIVE 强制 mux 所以 mux 的增强和分享已经提上日程了

@hippo2025
Copy link
Copy Markdown

If some of the DNS resolvers in the list become unavailable will it quickly switch to using the others?

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

@nnemirovsky You can keep the original multi-connection method, but it should be possible to modify only the client side without changing the server side.

Per review: separate UDP sockets per resolver avoids ISP source-port
rate limiting. Each resolver has its own recvLoop goroutine. Client-only
changes, server unchanged.
@nnemirovsky
Copy link
Copy Markdown
Author

@hippo2025 Currently it's round-robin without failover. If a resolver stops responding, queries sent to it are lost and KCP handles retransmission on the next round. With 3 resolvers and 1 dead, throughput drops ~33% but the tunnel stays up.

@LjhAUMEM Updated to use separate sockets per resolver (no server changes). Reverted all sync/server modifications from earlier.

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

@nnemirovsky Do you mind if I make changes directly on your branch?

@RPRX 话说我是不是没权限直接给他的分支提交

@Fangliding
Copy link
Copy Markdown
Member

单个文件的修改可以点铅笔 要大改我一般都是关了在本地重新弄一个 写co author就是了

@nnemirovsky
Copy link
Copy Markdown
Author

@LjhAUMEM go ahead, feel free to push directly to the branch.

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

单个文件的修改可以点铅笔 要大改我一般都是关了在本地重新弄一个 写co author就是了

我认为这样做可能不太礼貌,改的确实有点多,开新 pr 的话那晚点再说了

@LjhAUMEM go ahead, feel free to push directly to the branch.

I just tried and I don't have permission to commit. It seems I can only submit pull requests to your branch, but I already have a core fork, so it looks like I'll have to put this on hold for now.

@RPRX
Copy link
Copy Markdown
Member

RPRX commented Mar 31, 2026

@LjhAUMEM 给你 Maintain 权限了

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

LjhAUMEM commented Apr 1, 2026

@nnemirovsky I think we can start testing

{
  "log": { "loglevel": "debug" },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 1080,
      "protocol": "socks",
      "settings": {
        "auth": "noauth",
        "udp": true
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "vless",
      "settings": {
        "address": "127.0.0.1",
        "port": 53,
        "id": "5783a3e7-e373-51cd-8642-c83782b807c5",
        "encryption": "none"
      },
      "streamSettings": {
        "network": "kcp",
        "kcpSettings": {
          "mtu": 130
        },
        "finalmask": {
          "udp": [
            {
              "type": "xdns",
              "settings": {
                "domain": "", // 1
                "resolvers": [
                  "8.8.8.8:53", 
                  "1.1.1.1:53", 
                  "[2001:4860:4860::8888]:53",
                  "[2606:4700:4700::1111]:53"
                ]
              }
            }
          ]
        }
      }
    }
  ]
}
{
  "log": { "loglevel": "debug" },
  "inbounds": [
    {
      // "listen": "127.0.0.1",
      "port": 53,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "5783a3e7-e373-51cd-8642-c83782b807c5"
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "kcp",
        "kcpSettings": {
          "mtu": 900
        },
        "finalmask": {
          "udp": [
            {
              "type": "xdns",
              "settings": {
                "domain": "" // 1
              }
            }
          ]
        }
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom"
    }
  ]
}

@LjhAUMEM
Copy link
Copy Markdown
Collaborator

LjhAUMEM commented Apr 1, 2026

现在 xdns 和 xicmp 一样,都要在最外层,都忽略原始 conn,一个自动起 mult conn,一个起 ip conn,这两双子星越看越顺眼

只有一个区别,xicmp 会使用上层传进来的目标,xdns 则会忽略,只使用 resolvers 里的

那两个 kcp 的修改我不确定是不是必要的

还有个想法是把队列改成 wg bind 里的阻塞形式,不过再说了,改也不会在这个 pr

@nnemirovsky
Copy link
Copy Markdown
Author

nnemirovsky commented Apr 2, 2026

@LjhAUMEM both KCP changes are necessary for XDNS to work reliably. here's why:

120s timeout (connection.go)

mKCP's default 30s timeout is too short for DNS tunneling. the round-trip through public resolvers adds significant latency (DNS query -> resolver -> authoritative NS -> response -> resolver -> client). during TLS handshake, mKCP enters state 1 (handshaking) and times out at 30s before data can flow. confirmed with debug logging showing entering state 1 at 30000 consistently. 120s gives enough headroom for the handshake to complete even through slow resolvers.

cwnd*20 removal with congestion (sending.go)

the cwnd *= 20 multiplier allows 20x more packets in flight than the congestion window permits. without congestion control this is fine (KCP's default behavior for low-latency links), but when congestion: true is set, the multiplier defeats the purpose entirely. mKCP floods the XDNS sendLoop with retransmissions faster than it can drain through DNS queries, causing the write queue to overflow. removing the multiplier when congestion control is enabled lets the congestion window actually regulate the send rate, which is critical for bandwidth-limited transports like XDNS.

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.

5 participants