mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-05-04 11:32:28 +00:00
Add search domain support for Tailscale DNS
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,12 +48,14 @@ type DNSTransport struct {
|
||||
logger logger.ContextLogger
|
||||
endpointTag string
|
||||
acceptDefaultResolvers bool
|
||||
acceptSearchDomain bool
|
||||
dnsRouter adapter.DNSRouter
|
||||
endpointManager adapter.EndpointManager
|
||||
endpoint *Endpoint
|
||||
routePrefixes []netip.Prefix
|
||||
routes map[string][]adapter.DNSTransport
|
||||
hosts map[string][]netip.Addr
|
||||
searchDomains []string
|
||||
defaultResolvers []adapter.DNSTransport
|
||||
}
|
||||
|
||||
@@ -65,6 +69,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
|
||||
@@ -122,6 +127,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)
|
||||
@@ -132,12 +140,13 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
|
||||
}
|
||||
t.routes = routes
|
||||
t.hosts = hosts
|
||||
t.searchDomains = searchDomains
|
||||
t.defaultResolvers = defaultResolvers
|
||||
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
|
||||
}
|
||||
@@ -218,6 +227,39 @@ 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)
|
||||
}
|
||||
return t.exchangeOnce(ctx, message, t.acceptDefaultResolvers)
|
||||
}
|
||||
|
||||
func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
singleLabel := strings.TrimSuffix(message.Question[0].Name, ".")
|
||||
var lastErr error
|
||||
for _, searchDomain := range t.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]
|
||||
addresses, hostsLoaded := t.hosts[question.Name]
|
||||
if hostsLoaded {
|
||||
@@ -262,7 +304,7 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
return nil, lastErr
|
||||
}
|
||||
}
|
||||
if t.acceptDefaultResolvers {
|
||||
if allowDefaultResolvers {
|
||||
if len(t.defaultResolvers) > 0 {
|
||||
var lastErr error
|
||||
for _, resolver := range t.defaultResolvers {
|
||||
|
||||
Reference in New Issue
Block a user