feat(service): add smart_dns_pool — local DNS server fronting many upstreams#20
Open
hiddifyafk wants to merge 2 commits into
Open
feat(service): add smart_dns_pool — local DNS server fronting many upstreams#20hiddifyafk wants to merge 2 commits into
hiddifyafk wants to merge 2 commits into
Conversation
…streams
Adds a sing-box service `type: smart_dns_pool` that listens on a local
UDP+TCP address and fans incoming DNS queries out to many configured
recursive resolvers (UDP / TCP / DoT / DoH) using
github.com/hiddify/hmrd_multi_resolver_dns under the hood. The smart pool
provides:
- deadline-aware retry across resolvers within a single query,
- AIMD rate-limit throttling per resolver (REFUSED / SERVFAIL / HTTP
429 all decrement the cap; sustained success grows it back),
- a recovery prober that re-issues the offending query against a
"down" resolver every few seconds until it answers,
- round-robin / weighted / lowest-latency LB strategies.
Why: dnstt's tunnel sends DNS queries to a single recursive resolver
(e.g. `8.8.8.8`). When that resolver gets rate-limited or blocked by
the censor, the tunnel stalls until the operator rotates manually.
With this service, the operator configures the recursive resolvers
they want to distribute load across once, then sets dnstt's
`resolvers` to `udp://127.0.0.1:<listen_port>` instead of a fixed IP.
dnstt sees a normal local resolver; the pool transparently handles
failover and AIMD throttling for the real upstreams.
Sample config:
{
"services": [
{
"type": "smart_dns_pool",
"tag": "dnstt-pool",
"listen": "127.0.0.1",
"listen_port": 19876,
"load_balance": "weighted",
"upstreams": [
{ "type": "udp", "address": "1.1.1.1:53", "weight": 5 },
{ "type": "doh", "address": "https://cloudflare-dns.com/dns-query", "weight": 3 },
{ "type": "tls", "address": "1.1.1.1:853", "weight": 2 },
{ "type": "udp", "address": "8.8.8.8:53", "weight": 0 }
]
}
],
"outbounds": [
{ "type": "dnstt",
"tag": "dnstt-out",
"resolvers": [ { "type": "udp", "address": "127.0.0.1:19876" } ],
...
}
]
}
`weight: 0` means the upstream is a pure fallback — only reached when
every weighted candidate is unavailable.
Plumbing:
- constant/proxy.go — TypeSmartDNSPool = "smart_dns_pool"
- option/smart_dns_pool.go — SmartDNSPoolServiceOptions, the upstream
struct, and timeout / LB knobs
- service/smart_dns_pool/ — Service implementing adapter.Service:
Start binds the listener and starts the
multidns.Manager with all upstreams;
Close shuts the server and the manager
cleanly.
- include/registry.go — smartdnspool.RegisterService(registry)
Tests (`go test -race -count=1 ./service/smart_dns_pool/`):
- TestSmartDNSPool_EndToEnd full integration: builds the
service from real options, dig
@127.0.0.1 succeeds, round-robin
reaches both upstreams.
- TestSmartDNSPool_FailoverWithinDeadline one dropping + one healthy
upstream; queries through
the local listener still get
answered (the headline
dnstt use case).
- TestSmartDNSPool_RejectsBadConfig validation surfaces at NewService
time (no upstreams, missing port,
unsupported type).
`go.mod` pins github.com/hiddify/hmrd_multi_resolver_dns at the open
feat/packetconn-surface PR's SHA (v0.0.0-20260427043414-4f1e2d512cf6);
bump to a tagged version once that PR merges.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR hiddify#4 (the net.PacketConn surface) merged into hmrd_multi_resolver_dns main as 838b38fc. Switching the require from the feat-branch pseudo-version to a main-tip pseudo-version so the reference is from the canonical branch instead of a now-merged feature branch: github.com/hiddify/hmrd_multi_resolver_dns v0.0.0-20260427043414-4f1e2d512cf6 → v0.0.0-20260427063951-838b38fc5cf9 Equivalent code (the old SHA is reachable from the new one), but a cleaner audit trail. Tests still pass clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a sing-box service
type: smart_dns_poolthat listens on a local UDP+TCP address and fans incoming DNS queries out to many configured recursive resolvers (UDP / TCP / DoT / DoH) usinggithub.com/hiddify/hmrd_multi_resolver_dnsunder the hood.Replaces the closed #19 (which targeted the wrong layer). Integration shape now matches what was clarified offline: instead of dnstt asking sing-box's own DNS subsystem to resolve, you stand up this service locally and point dnstt's
resolversatudp://127.0.0.1:<listen_port>instead of8.8.8.8.Why this exists
dnstt's tunnel sends DNS queries to a single recursive resolver. When that resolver gets rate-limited or blocked by the censor, the tunnel stalls until the operator rotates manually. With this service the operator configures the recursive resolvers they want to distribute load across once, then sets dnstt's
resolversto the local listen address. dnstt sees a normal local resolver; the pool transparently handles:Rcode REFUSED/Rcode SERVFAIL/HTTP 429(DoH) all decrement the upstream's RPM cap (multiplicative-decrease); sustained success grows it back (additive-increase).down(after N consecutive timeouts/network failures), a background prober re-issues the offending query everyprobe_intervaluntil it answers, then promotes it back to healthy.roundrobin(default) /weighted/lowest_latency.weight: 0means the upstream is a pure fallback (only reached when every weighted candidate is unavailable).Sample config
{ "services": [ { "type": "smart_dns_pool", "tag": "dnstt-pool", "listen": "127.0.0.1", "listen_port": 19876, "load_balance": "weighted", "upstreams": [ { "type": "udp", "address": "1.1.1.1:53", "weight": 5 }, { "type": "doh", "address": "https://cloudflare-dns.com/dns-query", "weight": 3 }, { "type": "tls", "address": "1.1.1.1:853", "weight": 2 }, { "type": "udp", "address": "8.8.8.8:53", "weight": 0 } ] } ], "outbounds": [ { "type": "dnstt", "tag": "dnstt-out", "resolvers": [ { "type": "udp", "address": "127.0.0.1:19876" } ], "...": "..." } ] }Plumbing
constant/proxy.goTypeSmartDNSPool = "smart_dns_pool"option/smart_dns_pool.goSmartDNSPoolServiceOptions+SmartDNSPoolUpstreamOptions(listen + upstreams + LB / timeout knobs)service/smart_dns_pool/service.goServiceimplementingadapter.Service:Startbinds the listener and starts themultidns.Managerwith all upstreams;Closeshuts the server and the manager cleanlyservice/smart_dns_pool/service_test.goinclude/registry.gosmartdnspool.RegisterService(registry)go.modgithub.com/hiddify/hmrd_multi_resolver_dnsrequireTest plan
go vet ./service/smart_dns_pool/ ./include/ ./option/ ./constant/cleango build ./service/smart_dns_pool/ ./include/cleango test -race -count=1 ./service/smart_dns_pool/:TestSmartDNSPool_EndToEnd— builds service from realoption.SmartDNSPoolServiceOptions, sends an A query to127.0.0.1:<port>, verifies the answer routed through one of the configured upstreams. Round-robin reaches both upstreams over 10 queries.TestSmartDNSPool_FailoverWithinDeadline— one dropping + one healthy upstream; queries through the local listener still get answered (the headline dnstt failure mode).TestSmartDNSPool_RejectsBadConfig— validation surfaces atNewServicetime (no upstreams, missing port, unsupported upstream type).go test -racecovering failover, AIMD, prober recovery, custom dialer on every protocol, and thenet.PacketConnserver path this service relies on.Dependency note
go.modpins:That's the tip of
feat/packetconn-surface— the open library PR adding thenet.PacketConnsurface this service uses. Once that PR merges (and ideally a tag is cut on the library), this require can bump to the real version.Merge order
net.PacketConnsurface to the libraryextended