From 17c52d03b34207e83a5dd251515ec675371defe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 23 Sep 2026 22:09:48 +0800 Subject: [PATCH] Fix hysteria2 realm STUN resolution ignoring default domain resolver --- adapter/dns.go | 22 ------ common/dialer/dialer.go | 126 ++++++++++++++++----------------- option/hysteria2.go | 8 +++ protocol/hysteria2/inbound.go | 13 ++-- protocol/hysteria2/outbound.go | 18 +++-- 5 files changed, 90 insertions(+), 97 deletions(-) diff --git a/adapter/dns.go b/adapter/dns.go index cf949e3e..15df285d 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -8,9 +8,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - "github.com/sagernet/sing/service" "github.com/miekg/dns" ) @@ -45,26 +43,6 @@ type DNSQueryOptions struct { RemoveClientSubnet bool } -func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) { - if options == nil || options.Server == "" { - return DNSQueryOptions{}, nil - } - transportManager := service.FromContext[DNSTransportManager](ctx) - transport, loaded := transportManager.Transport(options.Server) - if !loaded { - return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server) - } - return DNSQueryOptions{ - Transport: transport, - Strategy: C.DomainStrategy(options.Strategy), - DisableCache: options.DisableCache, - DisableOptimisticCache: options.DisableOptimisticCache, - RewriteTTL: options.RewriteTTL, - Timeout: time.Duration(options.Timeout), - ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), - }, nil -} - type RDRCStore interface { LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) SaveRDRC(transportName string, qName string, qType uint16) error diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index f0e80430..7c0b9b5a 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -63,75 +63,31 @@ func NewWithOptions(options Options) (N.Dialer, error) { } } if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") { - networkManager := service.FromContext[adapter.NetworkManager](options.Context) - dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context) - var defaultOptions adapter.NetworkOptions - if networkManager != nil { - defaultOptions = networkManager.DefaultOptions() - } var ( - server string - dnsQueryOptions adapter.DNSQueryOptions - resolveFallbackDelay time.Duration + server string + dnsQueryOptions adapter.DNSQueryOptions ) - if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" { - var transport adapter.DNSTransport - if !options.DirectResolver { - var loaded bool - transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server) - if !loaded { - return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server) - } + hasDomainResolver := dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" + if options.DirectResolver { + if !hasDomainResolver { + return nil, E.New("missing domain resolver for domain server address") } - var strategy C.DomainStrategy - if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) { - strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy) - } else if - //nolint:staticcheck - dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { - //nolint:staticcheck - strategy = C.DomainStrategy(dialOptions.DomainStrategy) - deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) - } - server = dialOptions.DomainResolver.Server - dnsQueryOptions = adapter.DNSQueryOptions{ - Transport: transport, - Strategy: strategy, - Timeout: time.Duration(dialOptions.DomainResolver.Timeout), - DisableCache: dialOptions.DomainResolver.DisableCache, - DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache, - RewriteTTL: dialOptions.DomainResolver.RewriteTTL, - ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), - } - resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) - } else if options.DirectResolver { - return nil, E.New("missing domain resolver for domain server address") + dnsQueryOptions = domainResolveQueryOptions(dialOptions.DomainResolver) } else { - if defaultOptions.DomainResolver != "" { - dnsQueryOptions = defaultOptions.DomainResolveOptions - transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver) - if !loaded { - return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver) - } - dnsQueryOptions.Transport = transport - resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) - } else { - transports := dnsTransport.Transports() - if len(transports) < 2 { - dnsQueryOptions.Transport = dnsTransport.Default() - } else if options.NewDialer { - return nil, E.New("missing domain resolver for domain server address") - } else { - deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) - } + dnsQueryOptions, err = NewDNSQueryOptions(options.Context, dialOptions.DomainResolver, options.NewDialer) + if err != nil { + return nil, err } - if + } + if hasDomainResolver { + server = dialOptions.DomainResolver.Server + } + if + //nolint:staticcheck + dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) && (!hasDomainResolver || dialOptions.DomainResolver.Strategy == option.DomainStrategy(C.DomainStrategyAsIS)) { //nolint:staticcheck - dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { - //nolint:staticcheck - dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy) - deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) - } + dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy) + deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) } dialer = NewResolveDialer( options.Context, @@ -139,12 +95,54 @@ func NewWithOptions(options Options) (N.Dialer, error) { dialOptions.Detour == "" && !dialOptions.TCPFastOpen, server, dnsQueryOptions, - resolveFallbackDelay, + time.Duration(dialOptions.FallbackDelay), ) } return dialer, nil } +func NewDNSQueryOptions(ctx context.Context, domainResolver *option.DomainResolveOptions, newDialer bool) (adapter.DNSQueryOptions, error) { + dnsTransport := service.FromContext[adapter.DNSTransportManager](ctx) + if domainResolver != nil && domainResolver.Server != "" { + transport, loaded := dnsTransport.Transport(domainResolver.Server) + if !loaded { + return adapter.DNSQueryOptions{}, E.New("domain resolver not found: ", domainResolver.Server) + } + dnsQueryOptions := domainResolveQueryOptions(domainResolver) + dnsQueryOptions.Transport = transport + return dnsQueryOptions, nil + } + defaultOptions := service.FromContext[adapter.NetworkManager](ctx).DefaultOptions() + if defaultOptions.DomainResolver != "" { + transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver) + if !loaded { + return adapter.DNSQueryOptions{}, E.New("default domain resolver not found: ", defaultOptions.DomainResolver) + } + dnsQueryOptions := defaultOptions.DomainResolveOptions + dnsQueryOptions.Transport = transport + return dnsQueryOptions, nil + } + if len(dnsTransport.Transports()) < 2 { + return adapter.DNSQueryOptions{Transport: dnsTransport.Default()}, nil + } + if newDialer { + return adapter.DNSQueryOptions{}, E.New("missing domain resolver for domain server address") + } + deprecated.Report(ctx, deprecated.OptionMissingDomainResolver) + return adapter.DNSQueryOptions{}, nil +} + +func domainResolveQueryOptions(domainResolver *option.DomainResolveOptions) adapter.DNSQueryOptions { + return adapter.DNSQueryOptions{ + Strategy: C.DomainStrategy(domainResolver.Strategy), + Timeout: time.Duration(domainResolver.Timeout), + DisableCache: domainResolver.DisableCache, + DisableOptimisticCache: domainResolver.DisableOptimisticCache, + RewriteTTL: domainResolver.RewriteTTL, + ClientSubnet: domainResolver.ClientSubnet.Build(netip.Prefix{}), + } +} + type ParallelInterfaceDialer interface { N.Dialer DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) diff --git a/option/hysteria2.go b/option/hysteria2.go index 68832de9..237109b2 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -3,6 +3,7 @@ package option import ( "net/url" "reflect" + "slices" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/schema" @@ -10,6 +11,7 @@ import ( "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" ) type Hysteria2InboundOptions struct { @@ -37,6 +39,12 @@ type Hysteria2Realm struct { HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } +func (r Hysteria2Realm) STUNServersIsDomain() bool { + return slices.ContainsFunc(r.STUNServers, func(server string) bool { + return M.ParseSocksaddr(server).IsDomain() + }) +} + type Hysteria2RealmPortMapping struct { Enabled bool `json:"enabled,omitempty"` Timeout badoption.Duration `json:"timeout,omitempty"` diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index ef9233f3..977b5a9c 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" @@ -141,11 +142,15 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return nil, E.New("realm.ip_version 4 conflicts with listen address ", listenAddr) } } - queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.Realm.STUNDomainResolver) - if err != nil { - return nil, err + var queryOptions adapter.DNSQueryOptions + if options.Realm.STUNServersIsDomain() { + queryOptions, err = dialer.NewDNSQueryOptions(ctx, options.Realm.STUNDomainResolver, true) + if err != nil { + return nil, E.Cause(err, "create realm STUN domain resolver") + } } - httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) + var httpClientTransport adapter.HTTPTransport + httpClientTransport, err = service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) if err != nil { return nil, E.Cause(err, "create realm http client") } diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 6576bf9f..e235a472 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -76,21 +76,25 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } + realmSTUNServersIsDomain := options.Realm != nil && options.Realm.STUNServersIsDomain() outboundDialer, err := dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: options.DialerOptions, - RemoteIsDomain: options.ServerIsDomain(), + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain() || realmSTUNServersIsDomain, + ResolverOnDetour: realmSTUNServersIsDomain, + NewDialer: realmSTUNServersIsDomain, }) if err != nil { return nil, err } var realmOptions *realm.Options if options.Realm != nil { - queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.DialerOptions.DomainResolver) - if err != nil { - return nil, err + var queryOptions adapter.DNSQueryOptions + if realmSTUNServersIsDomain { + queryOptions = outboundDialer.(dialer.ResolveDialer).QueryOptions() } - httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) + var httpClientTransport adapter.HTTPTransport + httpClientTransport, err = service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) if err != nil { return nil, E.Cause(err, "create realm http client") }