diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md index 2677f2b82..b2169ed38 100644 --- a/docs/configuration/dns/server/tailscale.md +++ b/docs/configuration/dns/server/tailscale.md @@ -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" diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md index 10d84038c..e0086653d 100644 --- a/docs/configuration/dns/server/tailscale.zh.md +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -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" diff --git a/option/tailscale.go b/option/tailscale.go index f763c905d..a078e9aa8 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -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 { diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 3a92a66ba..8d5cad59b 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -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 {