From 0d8c15932fd92547dc8fbd8b3611053ed67582be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Jun 2025 18:23:10 +0800 Subject: [PATCH] Add options to custom DNS query timeout --- adapter/dns.go | 4 ++++ common/dialer/dialer.go | 1 + constant/timeout.go | 1 + dns/client.go | 16 +++++++++------- dns/router.go | 6 ++++++ dns/transport/dhcp/dhcp.go | 6 ++++++ dns/transport_adapter.go | 6 ++++++ option/outbound.go | 2 ++ option/rule_action.go | 2 ++ route/network.go | 1 + route/route.go | 1 + route/rule/rule_action.go | 29 +++++++++++++++++++---------- 12 files changed, 58 insertions(+), 17 deletions(-) diff --git a/adapter/dns.go b/adapter/dns.go index 4e79d657..cbe97a1f 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -3,6 +3,7 @@ package adapter import ( "context" "net/netip" + "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -36,6 +37,7 @@ type DNSQueryOptions struct { Transport DNSTransport Strategy C.DomainStrategy LookupStrategy C.DomainStrategy + Timeout time.Duration DisableCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix @@ -53,6 +55,7 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio return &DNSQueryOptions{ Transport: transport, Strategy: C.DomainStrategy(options.Strategy), + Timeout: time.Duration(options.Timeout), DisableCache: options.DisableCache, RewriteTTL: options.RewriteTTL, ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), @@ -70,6 +73,7 @@ type DNSTransport interface { Type() string Tag() string Dependencies() []string + HasDetour() bool Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 7bf8ff02..cbd26790 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -89,6 +89,7 @@ func NewWithOptions(options Options) (N.Dialer, error) { dnsQueryOptions = adapter.DNSQueryOptions{ Transport: transport, Strategy: strategy, + Timeout: time.Duration(dialOptions.DomainResolver.Timeout), DisableCache: dialOptions.DomainResolver.DisableCache, RewriteTTL: dialOptions.DomainResolver.RewriteTTL, ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), diff --git a/constant/timeout.go b/constant/timeout.go index eb0fd34c..211c1082 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -9,6 +9,7 @@ const ( TCPTimeout = 15 * time.Second ReadPayloadTimeout = 300 * time.Millisecond DNSTimeout = 10 * time.Second + DirectDNSTimeout = 5 * time.Second UDPTimeout = 5 * time.Minute DefaultURLTestInterval = 3 * time.Minute DefaultURLTestIdleTimeout = 30 * time.Minute diff --git a/dns/client.go b/dns/client.go index 27d8a027..fced8fde 100644 --- a/dns/client.go +++ b/dns/client.go @@ -30,7 +30,6 @@ var ( var _ adapter.DNSClient = (*Client)(nil) type Client struct { - timeout time.Duration disableCache bool disableExpire bool independentCache bool @@ -43,7 +42,6 @@ type Client struct { } type ClientOptions struct { - Timeout time.Duration DisableCache bool DisableExpire bool IndependentCache bool @@ -55,7 +53,6 @@ type ClientOptions struct { func NewClient(options ClientOptions) *Client { client := &Client{ - timeout: options.Timeout, disableCache: options.DisableCache, disableExpire: options.DisableExpire, independentCache: options.IndependentCache, @@ -63,9 +60,6 @@ func NewClient(options ClientOptions) *Client { initRDRCFunc: options.RDRC, logger: options.Logger, } - if client.timeout == 0 { - client.timeout = C.DNSTimeout - } cacheCapacity := options.CacheCapacity if cacheCapacity < 1024 { cacheCapacity = 1024 @@ -153,7 +147,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return nil, ErrResponseRejectedCached } } - ctx, cancel := context.WithTimeout(ctx, c.timeout) + timeout := options.Timeout + if timeout == 0 { + if transport.HasDetour() { + timeout = C.DNSTimeout + } else { + timeout = C.DirectDNSTimeout + } + } + ctx, cancel := context.WithTimeout(ctx, timeout) response, err := transport.Exchange(ctx, message) cancel() if err != nil { diff --git a/dns/router.go b/dns/router.go index 36b7bf05..7b76f115 100644 --- a/dns/router.go +++ b/dns/router.go @@ -158,6 +158,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } + if action.Timeout > 0 { + options.Timeout = action.Timeout + } if isFakeIP || action.DisableCache { options.DisableCache = true } @@ -180,6 +183,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } + if action.Timeout > 0 { + options.Timeout = action.Timeout + } if action.DisableCache { options.DisableCache = true } diff --git a/dns/transport/dhcp/dhcp.go b/dns/transport/dhcp/dhcp.go index 92dd1f8b..9d7579c3 100644 --- a/dns/transport/dhcp/dhcp.go +++ b/dns/transport/dhcp/dhcp.go @@ -41,6 +41,7 @@ type Transport struct { dns.TransportAdapter ctx context.Context dialer N.Dialer + hasDetour bool logger logger.ContextLogger networkManager adapter.NetworkManager interfaceName string @@ -59,6 +60,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions), ctx: ctx, dialer: transportDialer, + hasDetour: options.Detour != "", logger: logger, networkManager: service.FromContext[adapter.NetworkManager](ctx), interfaceName: options.Interface, @@ -89,6 +91,10 @@ func (t *Transport) Close() error { return nil } +func (t *Transport) HasDetour() bool { + return t.hasDetour +} + func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { err := t.fetchServers() if err != nil { diff --git a/dns/transport_adapter.go b/dns/transport_adapter.go index 47345709..855805ce 100644 --- a/dns/transport_adapter.go +++ b/dns/transport_adapter.go @@ -14,6 +14,7 @@ type TransportAdapter struct { transportType string transportTag string dependencies []string + hasDetour bool strategy C.DomainStrategy clientSubnet netip.Prefix } @@ -35,6 +36,7 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri transportType: transportType, transportTag: transportTag, dependencies: dependencies, + hasDetour: localOptions.Detour != "", strategy: C.DomainStrategy(localOptions.LegacyStrategy), clientSubnet: localOptions.LegacyClientSubnet, } @@ -69,6 +71,10 @@ func (a *TransportAdapter) Dependencies() []string { return a.dependencies } +func (a *TransportAdapter) HasDetour() bool { + return a.hasDetour +} + func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy { return a.strategy } diff --git a/option/outbound.go b/option/outbound.go index 2520d000..da915bfc 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -91,6 +91,7 @@ type DialerOptions struct { type _DomainResolveOptions struct { Server string `json:"server"` Strategy DomainStrategy `json:"strategy,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` @@ -102,6 +103,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) { if o.Server == "" { return []byte("{}"), nil } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && + o.Timeout == 0 && !o.DisableCache && o.RewriteTTL == nil && o.ClientSubnet == nil { diff --git a/option/rule_action.go b/option/rule_action.go index 914edb84..e7775ffd 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -180,6 +180,7 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { type DNSRouteActionOptions struct { Server string `json:"server,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` @@ -187,6 +188,7 @@ type DNSRouteActionOptions struct { type _DNSRouteOptionsActionOptions struct { Strategy DomainStrategy `json:"strategy,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` diff --git a/route/network.go b/route/network.go index 090e4c0d..75591102 100644 --- a/route/network.go +++ b/route/network.go @@ -76,6 +76,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp DomainResolver: defaultDomainResolver.Server, DomainResolveOptions: adapter.DNSQueryOptions{ Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), + Timeout: time.Duration(defaultDomainResolver.Timeout), DisableCache: defaultDomainResolver.DisableCache, RewriteTTL: defaultDomainResolver.RewriteTTL, ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), diff --git a/route/route.go b/route/route.go index 20fbf4ec..2654875c 100644 --- a/route/route.go +++ b/route/route.go @@ -666,6 +666,7 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{ Transport: transport, Strategy: action.Strategy, + Timeout: action.Timeout, DisableCache: action.DisableCache, RewriteTTL: action.RewriteTTL, ClientSubnet: action.ClientSubnet, diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index ce50dad9..458abc70 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -113,6 +113,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + Timeout: time.Duration(action.RouteOptions.Timeout), DisableCache: action.RouteOptions.DisableCache, RewriteTTL: action.RouteOptions.RewriteTTL, ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), @@ -121,6 +122,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), + Timeout: time.Duration(action.RouteOptionsOptions.Timeout), DisableCache: action.RouteOptionsOptions.DisableCache, RewriteTTL: action.RouteOptionsOptions.RewriteTTL, ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), @@ -235,20 +237,13 @@ func (r *RuleActionDNSRoute) Type() string { func (r *RuleActionDNSRoute) String() string { var descriptions []string descriptions = append(descriptions, r.Server) - if r.DisableCache { - descriptions = append(descriptions, "disable-cache") - } - if r.RewriteTTL != nil { - descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) - } - if r.ClientSubnet.IsValid() { - descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) - } + descriptions = append(descriptions, r.Descriptions()...) return F.ToString("route(", strings.Join(descriptions, ","), ")") } type RuleActionDNSRouteOptions struct { Strategy C.DomainStrategy + Timeout time.Duration DisableCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix @@ -259,7 +254,17 @@ func (r *RuleActionDNSRouteOptions) Type() string { } func (r *RuleActionDNSRouteOptions) String() string { + return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")") +} + +func (r *RuleActionDNSRouteOptions) Descriptions() []string { var descriptions []string + if r.Strategy != C.DomainStrategyAsIS { + descriptions = append(descriptions, F.ToString("strategy=", option.DomainStrategy(r.Strategy))) + } + if r.Timeout > 0 { + descriptions = append(descriptions, F.ToString("timeout=", r.Timeout.String())) + } if r.DisableCache { descriptions = append(descriptions, "disable-cache") } @@ -269,7 +274,7 @@ func (r *RuleActionDNSRouteOptions) String() string { if r.ClientSubnet.IsValid() { descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) } - return F.ToString("route-options(", strings.Join(descriptions, ","), ")") + return descriptions } type RuleActionDirect struct { @@ -421,6 +426,7 @@ func (r *RuleActionSniff) String() string { type RuleActionResolve struct { Server string Strategy C.DomainStrategy + Timeout time.Duration DisableCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix @@ -438,6 +444,9 @@ func (r *RuleActionResolve) String() string { if r.Strategy != C.DomainStrategyAsIS { options = append(options, F.ToString(option.DomainStrategy(r.Strategy))) } + if r.Timeout > 0 { + options = append(options, F.ToString("timeout=", r.Timeout.String())) + } if r.DisableCache { options = append(options, "disable_cache") }