Add search domain support for Tailscale DNS

This commit is contained in:
世界
2026-04-20 09:39:32 +08:00
parent 6ecd3deef5
commit b358fdd564
4 changed files with 80 additions and 6 deletions
+14 -1
View File
@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [accept_search_domain](#accept_search_domain)
!!! question "Since sing-box 1.12.0"
# Tailscale
@@ -17,7 +21,8 @@ icon: material/new-box
"tag": "",
"endpoint": "ts-ep",
"accept_default_resolvers": false
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
@@ -38,6 +43,14 @@ Indicates whether default DNS resolvers should be accepted for fallback queries
if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
#### accept_search_domain
!!! question "Since sing-box 1.14.0"
When enabled, single-label queries (e.g. `my-device`) are retried against each Tailscale search domain until one resolves.
Default resolvers are not consulted for single-label queries regardless of `accept_default_resolvers`.
### Examples
=== "MagicDNS only"
+14 -1
View File
@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [accept_search_domain](#accept_search_domain)
!!! question "自 sing-box 1.12.0 起"
# Tailscale
@@ -17,7 +21,8 @@ icon: material/new-box
"tag": "",
"endpoint": "ts-ep",
"accept_default_resolvers": false
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
@@ -38,6 +43,14 @@ icon: material/new-box
如果未启用,对于非 Tailscale 域名查询将返回 `NXDOMAIN`
#### accept_search_domain
!!! question "自 sing-box 1.14.0 起"
启用后,单标签查询(例如 `my-device`)将依次附加 Tailscale 搜索域进行重试,直到其中一个解析成功。
对于单标签查询,无论 `accept_default_resolvers` 是否启用,都不会使用默认 DNS 解析器。
### 示例
=== "仅 MagicDNS"
+1
View File
@@ -36,6 +36,7 @@ type TailscaleEndpointOptions struct {
type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
type TailscaleCertificateProviderOptions struct {
+51 -4
View File
@@ -4,6 +4,7 @@ package tailscale
import (
"context"
"errors"
"net"
"net/http"
"net/netip"
@@ -28,6 +29,7 @@ import (
"github.com/sagernet/sing/service"
nDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/types/dnstype"
"github.com/sagernet/tailscale/util/dnsname"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/sagernet/tailscale/wgengine/wgcfg"
@@ -46,6 +48,7 @@ type DNSTransport struct {
logger logger.ContextLogger
endpointTag string
acceptDefaultResolvers bool
acceptSearchDomain bool
dnsRouter adapter.DNSRouter
endpointManager adapter.EndpointManager
endpoint *Endpoint
@@ -53,6 +56,7 @@ type DNSTransport struct {
routePrefixes []netip.Prefix
routes map[string][]adapter.DNSTransport
hosts map[string][]netip.Addr
searchDomains []string
defaultResolvers []adapter.DNSTransport
}
@@ -66,6 +70,7 @@ func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string,
logger: logger,
endpointTag: options.Endpoint,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
acceptSearchDomain: options.AcceptSearchDomain,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
endpointManager: service.FromContext[adapter.EndpointManager](ctx),
}, nil
@@ -129,6 +134,9 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
for domain, addresses := range dnsConfig.Hosts {
hosts[domain.WithTrailingDot()] = addresses
}
searchDomains := common.Map(dnsConfig.SearchDomains, func(it dnsname.FQDN) string {
return it.WithTrailingDot()
})
var defaultResolvers []adapter.DNSTransport
for _, resolver := range dnsConfig.DefaultResolvers {
myResolver, err := t.createResolver(directDialerOnce, resolver)
@@ -143,6 +151,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
t.routePrefixes = routePrefixes
t.routes = routes
t.hosts = hosts
t.searchDomains = searchDomains
t.defaultResolvers = defaultResolvers
t.access.Unlock()
@@ -151,10 +160,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
}
if len(defaultResolvers) > 0 {
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, ", len(searchDomains), " search domains, default resolvers: ",
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
} else {
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts")
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, ", len(searchDomains), " search domains")
}
return nil
}
@@ -250,13 +259,51 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
if len(message.Question) != 1 {
return nil, os.ErrInvalid
}
if t.acceptSearchDomain && mDNS.CountLabel(message.Question[0].Name) == 1 {
return t.exchangeWithSearchDomains(ctx, message)
}
t.access.RLock()
acceptDefaultResolvers := t.acceptDefaultResolvers
t.access.RUnlock()
return t.exchangeOnce(ctx, message, acceptDefaultResolvers)
}
func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.access.RLock()
searchDomains := t.searchDomains
t.access.RUnlock()
singleLabel := strings.TrimSuffix(message.Question[0].Name, ".")
var lastErr error
for _, searchDomain := range searchDomains {
question := message.Question[0]
question.Name = singleLabel + "." + searchDomain
rewritten := *message
rewritten.Question = []mDNS.Question{question}
response, err := t.exchangeOnce(ctx, &rewritten, false)
if err == nil {
if response.Rcode == mDNS.RcodeNameError {
continue
}
return response, nil
}
if errors.Is(err, dns.RcodeNameError) {
continue
}
lastErr = err
}
if lastErr != nil {
return nil, lastErr
}
return nil, dns.RcodeNameError
}
func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allowDefaultResolvers bool) (*mDNS.Msg, error) {
question := message.Question[0]
t.access.RLock()
hosts := t.hosts
routes := t.routes
defaultResolvers := t.defaultResolvers
acceptDefaultResolvers := t.acceptDefaultResolvers
t.access.RUnlock()
addresses, hostsLoaded := hosts[question.Name]
@@ -302,7 +349,7 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
return nil, lastErr
}
}
if acceptDefaultResolvers {
if allowDefaultResolvers {
if len(defaultResolvers) > 0 {
var lastErr error
for _, resolver := range defaultResolvers {