diff --git a/adapter/dns.go b/adapter/dns.go index afee5aa8a..a613de0c4 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -42,16 +42,16 @@ type DNSQueryOptions struct { ClientSubnet netip.Prefix } -func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { - if options == nil { - return &DNSQueryOptions{}, nil +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 nil, E.New("domain resolver not found: " + options.Server) + return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server) } - return &DNSQueryOptions{ + return DNSQueryOptions{ Transport: transport, Strategy: C.DomainStrategy(options.Strategy), DisableCache: options.DisableCache, diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 4d2494022..0769e3c40 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -46,6 +46,7 @@ icon: material/alert-decagram "token": "", "realm_id": "", "stun_servers": [], + "stun_domain_resolver": "", // or {} "http_client": {} } } @@ -209,7 +210,15 @@ Outbounds must use the same `realm_id` to find this server. List of STUN servers (`host` or `host:port`) used to discover public addresses. -Port defaults to `3478`. +#### realm.stun_domain_resolver + +Set domain resolver to use for resolving STUN server domain names. + +This option uses the same format as the [route DNS rule action](/configuration/dns/rule_action/#route) without the `action` field. + +Setting this option directly to a string is equivalent to setting `server` of this options. + +If empty, the default domain resolver is used. #### realm.http_client diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index cfe7f9f0d..4af8a9eed 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -46,6 +46,7 @@ icon: material/alert-decagram "token": "", "realm_id": "", "stun_servers": [], + "stun_domain_resolver": "", // 或 {} "http_client": {} } } @@ -206,7 +207,15 @@ Realm 上的槽位标识符。 用于发现公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 -端口默认为 `3478`。 +#### realm.stun_domain_resolver + +用于解析 STUN 服务器域名的域名解析器。 + +此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 + +若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 + +如果为空,则使用默认域名解析器。 #### realm.http_client diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 4e162b554..3bcd42eaa 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -191,7 +191,7 @@ The same slot identifier the target Hysteria2 server registered. List of STUN servers (`host` or `host:port`) used to discover this client's public addresses. -Port defaults to `3478`. +Domain names are resolved using [`domain_resolver`](/configuration/shared/dial/#domain_resolver) from Dial Fields. #### realm.http_client diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index aff60a8a1..a39f3be50 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -189,7 +189,7 @@ Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配 用于发现本客户端公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 -端口默认为 `3478`。 +域名通过 [拨号字段](/zh/configuration/shared/dial/) 中的 [`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver) 解析。 #### realm.http_client diff --git a/go.mod b/go.mod index bac338d09..1f365c4f6 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 + github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index be6ebec39..03a2cfb73 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 h1:bwja5I7Sgr4Z1nyCLlN6T5eBhhSE/ilYZmkEFIoPAhQ= -github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= +github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 h1:P1iab6udg2I2igIrn+mNKpPZNcuejSqno3jwJ/94upw= +github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/option/hysteria2.go b/option/hysteria2.go index eee810543..b3f1208ee 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -19,10 +19,10 @@ type Hysteria2InboundOptions struct { IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer QUICOptions - Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` - BBRProfile string `json:"bbr_profile,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` - Realm *Hysteria2Realm `json:"realm,omitempty"` + Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` + Realm *Hysteria2InboundRealm `json:"realm,omitempty"` } type Hysteria2Realm struct { @@ -33,6 +33,11 @@ type Hysteria2Realm struct { HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } +type Hysteria2InboundRealm struct { + Hysteria2Realm + STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"` +} + type Hysteria2Obfs struct { Type string `json:"type,omitempty"` Password string `json:"password,omitempty"` diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index a6e800c5a..d18b3fa62 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/http/httputil" + "net/netip" "net/url" "time" @@ -18,11 +19,13 @@ import ( qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) func RegisterInbound(registry *inbound.Registry) { @@ -114,9 +117,35 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) - if err != nil { - return nil, err + var realmOptions *realm.Options + if options.Realm != nil { + queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.Realm.STUNDomainResolver) + if err != nil { + return nil, err + } + 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") + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + realmOptions = &realm.Options{ + ServerURL: options.Realm.ServerURL, + Token: options.Realm.Token, + RealmID: options.Realm.RealmID, + STUNServers: options.Realm.STUNServers, + HTTPClient: &http.Client{Transport: httpClientTransport}, + Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) { + dnsOptions := queryOptions + switch { + case ipv4 && !ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv4Only + case !ipv4 && ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv6Only + } + return dnsRouter.Lookup(ctx, host, dnsOptions) + }, + Logger: logger, + } } hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ Context: ctx, diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 28bf4a5c7..f4d4d58d3 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -3,6 +3,8 @@ package hysteria2 import ( "context" "net" + "net/http" + "net/netip" "net/url" "os" "time" @@ -18,12 +20,14 @@ import ( qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) func RegisterOutbound(registry *outbound.Registry) { @@ -66,13 +70,43 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } - outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + }) if err != nil { return nil, err } - realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) - 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 + } + 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") + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + realmOptions = &realm.Options{ + ServerURL: options.Realm.ServerURL, + Token: options.Realm.Token, + RealmID: options.Realm.RealmID, + STUNServers: options.Realm.STUNServers, + HTTPClient: &http.Client{Transport: httpClientTransport}, + Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) { + dnsOptions := queryOptions + switch { + case ipv4 && !ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv4Only + case !ipv4 && ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv6Only + } + return dnsRouter.Lookup(ctx, host, dnsOptions) + }, + Logger: logger, + } } networkList := options.Network.Build() client, err := hysteria2.NewClient(hysteria2.ClientOptions{ diff --git a/protocol/hysteria2/realm.go b/protocol/hysteria2/realm.go index 7eb0f7425..960bfbc91 100644 --- a/protocol/hysteria2/realm.go +++ b/protocol/hysteria2/realm.go @@ -13,13 +13,11 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" sHTTP "github.com/sagernet/sing/protocol/http" - "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -31,24 +29,6 @@ func RegisterRealmService(registry *boxService.Registry) { boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService) } -func buildRealmOptions(ctx context.Context, logger log.ContextLogger, options *option.Hysteria2Realm) (*realm.Options, error) { - if options == nil { - return nil, nil - } - transport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.HTTPClient)) - if err != nil { - return nil, E.Cause(err, "create realm http client") - } - return &realm.Options{ - ServerURL: options.ServerURL, - Token: options.Token, - RealmID: options.RealmID, - STUNServers: options.STUNServers, - HTTPClient: &http.Client{Transport: transport}, - Logger: logger, - }, nil -} - type RealmService struct { boxService.Adapter ctx context.Context