Skip to content

feat(dnstt): integrate hmrd_multi_resolver_dns smart pool inside dnstt#21

Merged
hiddify-com merged 1 commit into
hiddify:extendedfrom
QuantIntellect:claude/inspiring-turing-9eebae
Apr 29, 2026
Merged

feat(dnstt): integrate hmrd_multi_resolver_dns smart pool inside dnstt#21
hiddify-com merged 1 commit into
hiddify:extendedfrom
QuantIntellect:claude/inspiring-turing-9eebae

Conversation

@QuantIntellect
Copy link
Copy Markdown

Summary

Replaces the prior shape — a separate `smart_dns_pool` sing-box service (#20) and the earlier top-level `hmrd` DNS transport — with an in-dnstt integration of `hiddify/hmrd_multi_resolver_dns`.

When `smart_pool.enabled` is set on a `dnstt` outbound, every working resolver added via `addResolver` is registered as an upstream of an internal multidns pool. The dnstt tunnel then speaks to a single virtual resolver pointing at a local `127.0.0.1:` listener; the pool transparently handles deadline-aware failover, AIMD rate-limit throttling (REFUSED / SERVFAIL / HTTP 429), and recovery probing across the upstreams — replacing dnstt's own per-resolver tunnel distribution.

Why

dnstt's tunnel sends DNS queries to one resolver at a time and has no smart routing across them — when a resolver gets rate-limited or blocked, the tunnel stalls until the operator rotates manually. The manager wanted this fixed inline at the resolver-pool level, not behind a separate sing-box service the operator has to wire up. The integration sits at exactly the line he pointed at: `addResolver` in `protocol/hiddify/dnstt/dnstt_func.go`.

Sample config

{
  "outbounds": [
    {
      "type": "dnstt",
      "tag":  "dnstt-out",
      "domain": "t.example.com",
      "pubkey": "...",
      "resolvers": [
        "1.1.1.1:53",
        "8.8.8.8:53",
        "https://cloudflare-dns.com/dns-query",
        "dot://1.1.1.1:853"
      ],
      "smart_pool": {
        "enabled": true,
        "load_balance": "weighted",
        "deadline": "5s",
        "per_attempt": "2s",
        "probe_interval": "5s",
        "down_after": 8
      }
    }
  ]
}

Leaving `smart_pool` unset preserves the existing per-resolver tunnel distribution — default behavior is unchanged.

Plumbing

File Change
option/dnstt.go `SmartPool *DnsttSmartPoolOptions` block (`enabled` + LB / timeout knobs)
`protocol/hiddify/dnstt/smart_pool.go` (new) `multidns.Manager` + internal UDP/TCP listener; `dnstt.Resolver` → `multidns.ResolverConfig` translator; sing-box `ContextLogger` → `multidns.Logger` adapter
protocol/hiddify/dnstt/outbound.go `Outbound` carries `*smartPool`; built in `NewOutbound` when enabled; torn down in `Close`
protocol/hiddify/dnstt/dnstt_func.go `addResolver` registers each working resolver with the pool and ensures `c.resolvers` holds a single virtual resolver pointing at the local listener
`go.mod` / `go.sum` adds `github.com/hiddify/hmrd_multi_resolver_dns v0.0.0-20260427063951-838b38fc5cf9`

The library require pulls go 1.25, so `go mod tidy` bumped the toolchain line from 1.24.7 to 1.25.6.

What's deliberately not here

  • No standalone `smart_dns_pool` sing-box service (#20's shape — the rejected one).
  • No top-level `hmrd` DNS transport.
  • No new user-facing fields beyond the single `smart_pool` block on the existing dnstt outbound. The user's existing `resolvers: [...]` list keeps working unchanged.

Test plan

  • `go vet ./option/ ./protocol/hiddify/dnstt/` — clean
  • `go vet ./protocol/... ./option/...` — clean
  • `go build ./...` — clean (only pre-existing macOS linker warnings about duplicate `-lobjc`)
  • Live exercise: configure a dnstt outbound with multiple resolvers, set `smart_pool.enabled: true`, observe `smart_pool: registered as r-N` lines for each working resolver and tunnel traffic flowing through the local listener
  • Verify default path unchanged: same dnstt config without `smart_pool` block continues to tunnel as before

🤖 Generated with Claude Code

@QuantIntellect QuantIntellect force-pushed the claude/inspiring-turing-9eebae branch from 18b2332 to 445e384 Compare April 29, 2026 10:06
Comment thread protocol/hiddify/dnstt/outbound.go Outdated
// the dnstt tunnel can speak to as a single virtual resolver. Library
// defaults (5s deadline, 2s per-attempt, 5s probe, 8 failures = down,
// round-robin LB) cover the common case; tuning can land later.
func startSmartPool(ctx context.Context) (*multidns.Manager, string, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

اینو ببر توی hmrd

چون مشترکه بین همه

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in hiddify/hmrd_multi_resolver_dns#5 (merged) — moved into the library as multidns.StartLocal(opts) → (*Manager, addr, error). The dnstt outbound now just calls that. Bonus: it uses pre-bound net.PacketConn / net.Listener so the pick-port-then-rebind TOCTOU window is gone.

Comment thread protocol/hiddify/dnstt/dnstt_func.go Outdated
c.tunnels = append(c.tunnels, nil)
// }
}
c.mutlitunnel = nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

اینم حذف بشه چون تونل دیگه تغییر نمیکنه ریزالور هندل مبکنه

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

البته اگه اسمارت پول فعال بود نیاز نیست

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — c.mutlitunnel = nil is now only reset in the legacy path. In smart-pool mode the dnstt tunnel always points at the same virtual resolver, so the existing tunnel keeps running while the multidns pool rotates real upstreams behind it.

Comment thread protocol/hiddify/dnstt/dnstt_func.go Outdated
// Smart pool mode: register the resolver as a multidns upstream;
// c.resolvers stays at the single virtual resolver set up in
// NewOutbound, so dnstt's tunnel rides on top of the pool.
_, _ = c.mdMgr.AddResolver(multidns.ResolverConfig{Protocol: mdProto(resolver.ResolverType), Address: resolver.ResolverAddr})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

اینجا به نظرم ریزالور به صورت

https://x/dns-query

dot://ip:port
tcp://ip:port
udp://ip: port or ip:port

دریافت کنه بهتر باشه

تغییر درر
Hmrd
نیازه

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in hiddify/hmrd_multi_resolver_dns#5. Library now exposes multidns.AddResolverURL(s) accepting udp://, tcp://, dot:// (or tls://), https:// (or doh://), or bare host[:port] (defaults to UDP/53). dnstt calls it that way and the local mdProto switch is gone.

When `smart_pool: true` is set on a dnstt outbound, every working
resolver added via the existing addResolver flow is registered as an
upstream of an hmrd_multi_resolver_dns pool, and dnstt's tunnel speaks
to a single virtual resolver pointing at the local DNS listener the
pool fronts. The pool transparently handles deadline-aware failover,
AIMD rate-limit throttling (REFUSED / SERVFAIL / HTTP 429), and
recovery probing across the upstreams — replacing dnstt's own
per-resolver tunnel distribution.

Why: dnstt's tunnel sends DNS queries to one resolver at a time, so
when a resolver gets rate-limited or blocked the tunnel stalls until
the operator rotates manually. The integration sits at exactly the line
the manager pointed at — addResolver in dnstt_func.go — so the
user-visible config stays a regular dnstt outbound with one extra bool.

Plumbing:
  - option/dnstt.go                       — DnsttOptions.SmartPool bool
  - protocol/hiddify/dnstt/outbound.go    — `mdMgr *multidns.Manager`
                                            field; multidns.StartLocal
                                            on NewOutbound; Close
                                            tears the manager (and
                                            bundled listener) down
  - protocol/hiddify/dnstt/dnstt_func.go  — addResolver registers each
                                            working resolver via
                                            multidns.AddResolverURL
                                            (udp://, dot://, https://,
                                            or bare host:port for
                                            UDP/53). c.mutlitunnel
                                            isn't reset in this branch
                                            because the tunnel keeps
                                            pointing at the same
                                            virtual resolver — only
                                            the legacy path needs the
                                            tunnel rebuild.

go.mod adds github.com/hiddify/hmrd_multi_resolver_dns
v0.0.0-20260429114007-8d809dc33d0e. The library require pulls go 1.25,
so go mod tidy bumped the toolchain line accordingly.

Default behavior is unchanged: leaving smart_pool unset preserves the
existing per-resolver tunnel distribution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantIntellect QuantIntellect force-pushed the claude/inspiring-turing-9eebae branch from 445e384 to bccd6e7 Compare April 29, 2026 11:43
@hiddify-com hiddify-com merged commit e30f158 into hiddify:extended Apr 29, 2026
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.

2 participants