From 10879a4a8b202fb739ea9db6cb504f6a29f2b082 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 16 Oct 2026 06:41:19 +0800 Subject: [PATCH] XDNS finalmask: Support DoH ("https://", `tlsConfig`, domain fronting, etc., `quicConfig`, Chrome Client Hello by default) with empty HTTP User-Agent (#7140) https://github.com/XTLS/Xray-core/pull/7103#issuecomment-6118333973 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6161808702 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6167254749 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6185965142 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6200775375 --- infra/conf/transport_finalmask.go | 130 +++++++- infra/conf/transport_internet.go | 91 +----- .../internet/finalmask/xdns/config.pb.go | 61 +++- .../internet/finalmask/xdns/config.proto | 5 + transport/internet/finalmask/xdns/resolver.go | 2 + .../internet/finalmask/xdns/resolver_doh.go | 301 ++++++++++++++++++ transport/internet/tls/grpc.go | 2 +- transport/internet/tls/tls.go | 18 ++ 8 files changed, 503 insertions(+), 107 deletions(-) create mode 100644 transport/internet/finalmask/xdns/resolver_doh.go diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 7eeab9b08..046e03d01 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -9,12 +9,14 @@ import ( "fmt" "net/netip" "net/url" + "os" "regexp" "strings" googleuuid "github.com/google/uuid" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" @@ -28,6 +30,7 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask/xdns" "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" "github.com/xtls/xray-core/transport/internet/finalmask/xmc" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/tls" "google.golang.org/protobuf/proto" ) @@ -799,7 +802,10 @@ type XDNSDomain struct { } type XDNSResolver struct { - Addrs []string `json:"addrs"` + Addrs []string `json:"addrs"` + Worker int32 `json:"worker"` + TlsConfig *TLSConfig `json:"tlsConfig"` + QuicConfig *QuicParamsConfig `json:"quicConfig"` } type XDNS struct { @@ -834,6 +840,26 @@ func (c *XDNS) Build() (proto.Message, error) { } } for i := range c.Resolvers { + worker := c.Resolvers[i].Worker + if worker == 0 { + worker = 32 + } + var tlsConfig *tls.Config + var quicConfig *internet.QuicParams + if c.Resolvers[i].TlsConfig != nil { + proto, err := c.Resolvers[i].TlsConfig.Build() + if err != nil { + return nil, err + } + tlsConfig = proto.(*tls.Config) + } + if c.Resolvers[i].QuicConfig != nil { + proto, err := c.Resolvers[i].QuicConfig.Build() + if err != nil { + return nil, err + } + quicConfig = proto.(*internet.QuicParams) + } for j := range c.Resolvers[i].Addrs { var u *url.URL var e error @@ -845,18 +871,21 @@ func (c *XDNS) Build() (proto.Message, error) { if e != nil { return nil, e } + var addr string switch u.Scheme { case "tcp", "udp": + host := u.Hostname() + port := u.Port() + if port == "" { + port = "53" + } + addr = net.JoinHostPort(host, port) + case "https": + addr = u.String() default: return nil, errors.New("invalid protocol") } - var host, port string - host = u.Hostname() - port = u.Port() - if port == "" { - port = "53" - } - resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)}) + resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: addr, Worker: worker, TlsConfig: tlsConfig, QuicConfig: quicConfig}) } } if c.ExtraPoll < 0 || c.ExtraPoll > 3 { @@ -1149,6 +1178,91 @@ type QuicParamsConfig struct { DisableStatelessReset bool `json:"disableStatelessReset"` } +func (config *QuicParamsConfig) Build() (proto.Message, error) { + profile := strings.ToLower(config.BbrProfile) + switch profile { + case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive): + if profile == "" { + profile = string(bbr.ProfileStandard) + } + default: + return nil, errors.New("unknown bbr profile") + } + + up, err := config.BrutalUp.Bps() + if err != nil { + return nil, err + } + down, err := config.BrutalDown.Bps() + if err != nil { + return nil, err + } + + if up > 0 && up < 65536 { + return nil, errors.New("BrutalUp must be at least 65536 bytes per second") + } + if down > 0 && down < 65536 { + return nil, errors.New("BrutalDown must be at least 65536 bytes per second") + } + + config.Congestion = strings.ToLower(config.Congestion) + switch config.Congestion { + case "", "brutal", "reno", "bbr": + case "force-brutal": + if up == 0 { + return nil, errors.New("force-brutal requires up") + } + default: + return nil, errors.New("unknown congestion control: ", config.Congestion, ", valid values: reno, bbr, brutal, force-brutal") + } + + if config.InitStreamReceiveWindow > 0 && config.InitStreamReceiveWindow < 16384 { + return nil, errors.New("InitStreamReceiveWindow must be at least 16384") + } + if config.MaxStreamReceiveWindow > 0 && config.MaxStreamReceiveWindow < 16384 { + return nil, errors.New("MaxStreamReceiveWindow must be at least 16384") + } + if config.InitConnectionReceiveWindow > 0 && config.InitConnectionReceiveWindow < 16384 { + return nil, errors.New("InitConnectionReceiveWindow must be at least 16384") + } + if config.MaxConnectionReceiveWindow > 0 && config.MaxConnectionReceiveWindow < 16384 { + return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384") + } + if config.MaxIdleTimeout != 0 && (config.MaxIdleTimeout < 4 || config.MaxIdleTimeout > 120) { + return nil, errors.New("MaxIdleTimeout must be between 4 and 120") + } + if config.KeepAlivePeriod != 0 && (config.KeepAlivePeriod < 2 || config.KeepAlivePeriod > 60) { + return nil, errors.New("KeepAlivePeriod must be between 2 and 60") + } + if config.MaxIncomingStreams != 0 && config.MaxIncomingStreams < 8 { + return nil, errors.New("MaxIncomingStreams must be at least 8") + } + + if config.Debug { + os.Setenv("HYSTERIA_BBR_DEBUG", "true") + os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true") + } + + return &internet.QuicParams{ + Congestion: config.Congestion, + BbrProfile: profile, + BrutalUp: up, + BrutalDown: down, + BrutalDisableLossCompensation: config.BrutalDisableLossCompensation, + InitStreamReceiveWindow: config.InitStreamReceiveWindow, + MaxStreamReceiveWindow: config.MaxStreamReceiveWindow, + InitConnReceiveWindow: config.InitConnectionReceiveWindow, + MaxConnReceiveWindow: config.MaxConnectionReceiveWindow, + MaxIdleTimeout: config.MaxIdleTimeout, + KeepAlivePeriod: config.KeepAlivePeriod, + DisablePathMtuDiscovery: config.DisablePathMTUDiscovery, + DisableChromeParrot: config.DisableChromeParrot, + DisableGSO: config.DisableGSO, + MaxIncomingStreams: config.MaxIncomingStreams, + DisableStatelessReset: config.DisableStatelessReset, + }, nil +} + type FinalMask struct { Tcp []Mask `json:"tcp"` Udp []Mask `json:"udp"` diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 77992422c..ee1e5c3df 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1,13 +1,11 @@ package conf import ( - "os" "strings" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/transport/internet" - "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" ) type TransportProtocol string @@ -228,102 +226,25 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { if c.FinalMask != nil { for _, mask := range c.FinalMask.Tcp { - u, err := mask.Build(true) + proto, err := mask.Build(true) if err != nil { return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) } - config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u)) + config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto)) } for _, mask := range c.FinalMask.Udp { - u, err := mask.Build(false) + proto, err := mask.Build(false) if err != nil { return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) } - config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u)) + config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto)) } if c.FinalMask.QuicParams != nil { - profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile) - switch profile { - case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive): - if profile == "" { - profile = string(bbr.ProfileStandard) - } - default: - return nil, errors.New("unknown bbr profile") - } - - up, err := c.FinalMask.QuicParams.BrutalUp.Bps() + proto, err := c.FinalMask.QuicParams.Build() if err != nil { return nil, err } - down, err := c.FinalMask.QuicParams.BrutalDown.Bps() - if err != nil { - return nil, err - } - - if up > 0 && up < 65536 { - return nil, errors.New("BrutalUp must be at least 65536 bytes per second") - } - if down > 0 && down < 65536 { - return nil, errors.New("BrutalDown must be at least 65536 bytes per second") - } - - c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion) - switch c.FinalMask.QuicParams.Congestion { - case "", "brutal", "reno", "bbr": - case "force-brutal": - if up == 0 { - return nil, errors.New("force-brutal requires up") - } - default: - return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal") - } - - if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 { - return nil, errors.New("InitStreamReceiveWindow must be at least 16384") - } - if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 { - return nil, errors.New("MaxStreamReceiveWindow must be at least 16384") - } - if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 { - return nil, errors.New("InitConnectionReceiveWindow must be at least 16384") - } - if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 { - return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384") - } - if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) { - return nil, errors.New("MaxIdleTimeout must be between 4 and 120") - } - if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) { - return nil, errors.New("KeepAlivePeriod must be between 2 and 60") - } - if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 { - return nil, errors.New("MaxIncomingStreams must be at least 8") - } - - if c.FinalMask.QuicParams.Debug { - os.Setenv("HYSTERIA_BBR_DEBUG", "true") - os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true") - } - - config.QuicParams = &internet.QuicParams{ - Congestion: c.FinalMask.QuicParams.Congestion, - BbrProfile: profile, - BrutalUp: up, - BrutalDown: down, - BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation, - InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow, - MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow, - InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow, - MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow, - MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout, - KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod, - DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery, - DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot, - DisableGSO: c.FinalMask.QuicParams.DisableGSO, - MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams, - DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset, - } + config.QuicParams = proto.(*internet.QuicParams) } } diff --git a/transport/internet/finalmask/xdns/config.pb.go b/transport/internet/finalmask/xdns/config.pb.go index 9ef86ba4e..61e4f420e 100644 --- a/transport/internet/finalmask/xdns/config.pb.go +++ b/transport/internet/finalmask/xdns/config.pb.go @@ -8,6 +8,8 @@ package xdns import ( _ "github.com/xtls/xray-core/common/serial" + internet "github.com/xtls/xray-core/transport/internet" + tls "github.com/xtls/xray-core/transport/internet/tls" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -102,6 +104,9 @@ type ResolverProto struct { state protoimpl.MessageState `protogen:"open.v1"` Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` + Worker int32 `protobuf:"varint,3,opt,name=worker,proto3" json:"worker,omitempty"` + TlsConfig *tls.Config `protobuf:"bytes,4,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"` + QuicConfig *internet.QuicParams `protobuf:"bytes,5,opt,name=quic_config,json=quicConfig,proto3" json:"quic_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -150,6 +155,27 @@ func (x *ResolverProto) GetAddr() string { return "" } +func (x *ResolverProto) GetWorker() int32 { + if x != nil { + return x.Worker + } + return 0 +} + +func (x *ResolverProto) GetTlsConfig() *tls.Config { + if x != nil { + return x.TlsConfig + } + return nil +} + +func (x *ResolverProto) GetQuicConfig() *internet.QuicParams { + if x != nil { + return x.QuicConfig + } + return nil +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Domains []*DomainProto `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"` @@ -214,17 +240,22 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + "\n" + - ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\"\x8b\x01\n" + + ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\x1a#transport/internet/tls/config.proto\x1a\x1ftransport/internet/config.proto\"\x8b\x01\n" + "\vDomainProto\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" + "\tlen_limit\x18\x02 \x01(\x05R\blenLimit\x12\x1f\n" + "\vlabel_limit\x18\x03 \x01(\x05R\n" + "labelLimit\x12\x14\n" + "\x05types\x18\x04 \x03(\x05R\x05types\x12\x14\n" + - "\x05edns0\x18\x05 \x01(\x05R\x05edns0\"7\n" + + "\x05edns0\x18\x05 \x01(\x05R\x05edns0\"\xd9\x01\n" + "\rResolverProto\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" + - "\x04addr\x18\x02 \x01(\tR\x04addr\"\xcb\x01\n" + + "\x04addr\x18\x02 \x01(\tR\x04addr\x12\x16\n" + + "\x06worker\x18\x03 \x01(\x05R\x06worker\x12B\n" + + "\n" + + "tls_config\x18\x04 \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12D\n" + + "\vquic_config\x18\x05 \x01(\v2#.xray.transport.internet.QuicParamsR\n" + + "quicConfig\"\xcb\x01\n" + "\x06Config\x12M\n" + "\adomains\x18\x01 \x03(\v23.xray.transport.internet.finalmask.xdns.DomainProtoR\adomains\x12S\n" + "\tresolvers\x18\x02 \x03(\v25.xray.transport.internet.finalmask.xdns.ResolverProtoR\tresolvers\x12\x1d\n" + @@ -246,18 +277,22 @@ func file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP() []byte { var file_transport_internet_finalmask_xdns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_transport_internet_finalmask_xdns_config_proto_goTypes = []any{ - (*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto - (*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto - (*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config + (*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto + (*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto + (*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config + (*tls.Config)(nil), // 3: xray.transport.internet.tls.Config + (*internet.QuicParams)(nil), // 4: xray.transport.internet.QuicParams } var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{ - 0, // 0: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto - 1, // 1: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 0: xray.transport.internet.finalmask.xdns.ResolverProto.tls_config:type_name -> xray.transport.internet.tls.Config + 4, // 1: xray.transport.internet.finalmask.xdns.ResolverProto.quic_config:type_name -> xray.transport.internet.QuicParams + 0, // 2: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto + 1, // 3: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_transport_internet_finalmask_xdns_config_proto_init() } diff --git a/transport/internet/finalmask/xdns/config.proto b/transport/internet/finalmask/xdns/config.proto index e60ce8aba..2dea00dc1 100644 --- a/transport/internet/finalmask/xdns/config.proto +++ b/transport/internet/finalmask/xdns/config.proto @@ -7,6 +7,8 @@ option java_package = "com.xray.transport.internet.finalmask.xdns"; option java_multiple_files = true; import "common/serial/typed_message.proto"; +import "transport/internet/tls/config.proto"; +import "transport/internet/config.proto"; message DomainProto { string name = 1; @@ -19,6 +21,9 @@ message DomainProto { message ResolverProto { string type = 1; string addr = 2; + int32 worker = 3; + xray.transport.internet.tls.Config tls_config = 4; + xray.transport.internet.QuicParams quic_config = 5; } message Config { diff --git a/transport/internet/finalmask/xdns/resolver.go b/transport/internet/finalmask/xdns/resolver.go index 628dafbca..45b94e5ec 100644 --- a/transport/internet/finalmask/xdns/resolver.go +++ b/transport/internet/finalmask/xdns/resolver.go @@ -20,6 +20,8 @@ func NewResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, err return NewTCPResolver(config, dialer) case "udp": return NewUDPResolver(config, dialer) + case "https": + return NewDOHResolver(config, dialer) default: return nil, errors.New("unknown type") } diff --git a/transport/internet/finalmask/xdns/resolver_doh.go b/transport/internet/finalmask/xdns/resolver_doh.go new file mode 100644 index 000000000..990a12e26 --- /dev/null +++ b/transport/internet/finalmask/xdns/resolver_doh.go @@ -0,0 +1,301 @@ +package xdns + +import ( + "bytes" + "context" + gotls "crypto/tls" + "io" + "net/http" + "runtime" + "strconv" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type DOHResolver struct { + ctx context.Context + cancel func() + client *http.Client + url string + time time.Time + + udpAddr *net.UDPAddr + readCh chan []byte + sendCh chan []byte + closeCh chan struct{} + wg sync.WaitGroup + mu sync.Mutex +} + +func NewDOHResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, error) { + if config.Worker < 1 { + return nil, errors.New("invalid worker") + } + if config.TlsConfig == nil { + return nil, errors.New("empty tls config") + } + ctx, cancel := context.WithCancel(context.Background()) + var tr http.RoundTripper + var setaddr func(addr net.Addr) + switch { + case len(config.TlsConfig.NextProtocol) == 1 && config.TlsConfig.NextProtocol[0] == "h3": + quicParams := config.QuicConfig + if quicParams == nil { + quicParams = &internet.QuicParams{} + } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, + MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, + KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, + DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), + ChromeParrot: !quicParams.DisableChromeParrot, + } + tr = &http3.Transport{ + TLSClientConfig: config.TlsConfig.GetTLSConfig(), + QUICConfig: quicConfig, + Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) { + dest, err := net.ParseDestination("udp:" + addr) + if err != nil { + return nil, err + } + conn, err := dialer.DialUDP(dest) + if err != nil { + return nil, err + } + setaddr(conn.RemoteAddr()) + tr := &quic.Transport{Conn: conn.(*net.PacketConnWrapper).PacketConn, DisableGSO: quicParams.DisableGSO} + if !quicParams.DisableChromeParrot { + tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{} + tlsCfg.GetCertificate = nil + } + qc, err := tr.Dial(ctx, conn.RemoteAddr(), tlsCfg, cfg) + if err != nil { + return nil, err + } + context.AfterFunc(qc.Context(), func() { tr.Close(); conn.Close() }) + switch quicParams.Congestion { + case "", "reno": + case "bbr": + congestion.UseBBR(qc, bbr.Profile(quicParams.BbrProfile)) + case "force-brutal": + congestion.UseBrutal(qc, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation) + default: + panic(quicParams.Congestion) + } + return qc, nil + }, + } + default: + tlsConfig := config.TlsConfig.GetTLSConfig() + tr = &http.Transport{ + ForceAttemptHTTP2: true, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := net.ParseDestination("tcp:" + addr) + if err != nil { + return nil, err + } + conn, err := dialer.DialTCP(dest) + if err != nil { + return nil, err + } + setaddr(conn.RemoteAddr()) + var tlsConn net.Conn + if fingerprint := tls.GetFingerprint(config.TlsConfig.Fingerprint); fingerprint != nil { + tlsConn = tls.UClient(conn, tlsConfig, fingerprint) + err = tlsConn.(*tls.UConn).HandshakeContext(ctx) + } else { + tlsConn = gotls.Client(conn, tlsConfig) + err = tlsConn.(*gotls.Conn).HandshakeContext(ctx) + } + if err != nil { + _ = conn.Close() + return nil, err + } + return tlsConn, nil + }, + } + } + client := &http.Client{ + Transport: tr, + } + r := &DOHResolver{ + ctx: ctx, + cancel: cancel, + client: client, + url: config.Addr, + + udpAddr: &net.UDPAddr{IP: []byte{0, 0, 0, 0}}, + readCh: make(chan []byte), + sendCh: make(chan []byte), + closeCh: make(chan struct{}), + } + for range config.Worker { + r.wg.Add(1) + go r.worker() + } + setaddr = r.setaddr + return r, nil +} + +func (r *DOHResolver) closed() bool { + select { + case <-r.closeCh: + return true + default: + return false + } +} + +func (r *DOHResolver) setaddr(addr net.Addr) { + r.mu.Lock() + defer r.mu.Unlock() + switch v := addr.(type) { + case *net.TCPAddr: + r.udpAddr = &net.UDPAddr{IP: v.IP, Port: v.Port} + case *net.UDPAddr: + r.udpAddr = v + default: + panic(v) + } +} + +func (r *DOHResolver) cooldown(t time.Time) bool { + r.mu.Lock() + defer r.mu.Unlock() + if t == (time.Time{}) { + return time.Now().Before(r.time) + } + if t.After(r.time) { + r.time = t + } + return true +} + +func (r *DOHResolver) do(p []byte) error { + req := common.Must2(http.NewRequestWithContext(r.ctx, "POST", r.url, bytes.NewReader(p))) + req.Header.Set("Accept", "application/dns-message") + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("User-Agent", "") + resp, err := r.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + p := pool4K.Get().([]byte) + off := 0 + for { + n, err := resp.Body.Read(p[off:]) + if n > 0 { + off += n + } + if off == len(p) || err != nil { + if err != io.EOF { + pool4K.Put(p[:cap(p)]) + return err + } + break + } + } + select { + case <-r.closeCh: + pool4K.Put(p[:cap(p)]) + case r.readCh <- p[:off]: + } + return nil + case http.StatusTooManyRequests: + if value := resp.Header.Get("Retry-After"); value != "" { + if t, err := http.ParseTime(value); err == nil { + r.cooldown(t) + } else if i, err := strconv.ParseUint(value, 10, 32); err == nil { + r.cooldown(time.Now().Add(time.Duration(i) * time.Second)) + } + } + } + return errors.New("code ", resp.StatusCode, " header ", resp.Header) +} + +func (r *DOHResolver) worker() { + defer r.wg.Done() + + var p []byte + for { + select { + case <-r.closeCh: + return + default: + select { + case <-r.closeCh: + return + case p = <-r.sendCh: + } + } + if r.cooldown(time.Time{}) { + continue + } + if err := r.do(p); err != nil { + errors.LogErrorInner(context.Background(), err, "do err") + } + } +} + +func (r *DOHResolver) Addr() *net.UDPAddr { + r.mu.Lock() + defer r.mu.Unlock() + return r.udpAddr +} + +func (r *DOHResolver) Read(p []byte) (n int, err error) { + packet, ok := <-r.readCh + if ok { + n = copy(p, packet) + pool4K.Put(packet[:cap(packet)]) + return n, nil + } + return 0, io.ErrClosedPipe +} + +func (r *DOHResolver) Send(p []byte) { + select { + case <-r.closeCh: + return + default: + b := make([]byte, len(p)) + copy(b, p) + select { + case <-r.closeCh: + return + case r.sendCh <- b: + } + } +} + +func (r *DOHResolver) Close() { + r.mu.Lock() + if r.closed() { + r.mu.Unlock() + return + } + close(r.closeCh) + r.mu.Unlock() + r.cancel() + r.wg.Wait() + r.client.CloseIdleConnections() + close(r.readCh) + // close(r.sendCh) +} diff --git a/transport/internet/tls/grpc.go b/transport/internet/tls/grpc.go index 6e5dc578c..5a437bfc0 100644 --- a/transport/internet/tls/grpc.go +++ b/transport/internet/tls/grpc.go @@ -14,7 +14,7 @@ import ( // grpcUtlsInfo contains the auth information for a TLS authenticated connection. // It implements the AuthInfo interface. type grpcUtlsInfo struct { - State utls.ConnectionState + State gotls.ConnectionState credentials.CommonAuthInfo // This API is experimental. SPIFFEID *url.URL diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 6ca951327..cd5fe4c23 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -154,6 +154,24 @@ func (c *UConn) NegotiatedProtocol() string { return state.NegotiatedProtocol } +func (c *UConn) ConnectionState() tls.ConnectionState { + s := c.UConn.ConnectionState() + var state tls.ConnectionState + state.HandshakeComplete = s.HandshakeComplete + state.Version = s.Version + state.NegotiatedProtocol = s.NegotiatedProtocol + state.DidResume = s.DidResume + state.ServerName = s.ServerName + state.CipherSuite = s.CipherSuite + state.PeerCertificates = s.PeerCertificates + state.VerifiedChains = s.VerifiedChains + state.SignedCertificateTimestamps = s.SignedCertificateTimestamps + state.OCSPResponse = s.OCSPResponse + state.TLSUnique = s.TLSUnique + state.ECHAccepted = s.ECHAccepted + return state +} + func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn { utlsConn := utls.UClient(c, copyConfig(config), *fingerprint) return &UConn{UConn: utlsConn}