99 "sync"
1010 "time"
1111
12+ "github.com/quic-go/quic-go"
13+ "github.com/quic-go/quic-go/http3"
1214 "github.com/xtls/xray-core/common"
1315 "github.com/xtls/xray-core/common/buf"
1416 c "github.com/xtls/xray-core/common/ctx"
@@ -24,6 +26,13 @@ import (
2426 "golang.org/x/net/http2"
2527)
2628
29+ // defines the maximum time an idle TCP session can survive in the tunnel, so
30+ // it should be consistent across HTTP versions and with other transports.
31+ const connIdleTimeout = 300 * time .Second
32+
33+ // consistent with quic-go
34+ const h3KeepalivePeriod = 10 * time .Second
35+
2736type dialerConf struct {
2837 net.Destination
2938 * internet.MemoryStreamConfig
@@ -48,72 +57,129 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
4857 if tlsConfigs == nil && realityConfigs == nil {
4958 return nil , errors .New ("TLS or REALITY must be enabled for http transport." ).AtWarning ()
5059 }
60+ isH3 := tlsConfigs != nil && (len (tlsConfigs .NextProtocol ) == 1 && tlsConfigs .NextProtocol [0 ] == "h3" )
61+ if isH3 {
62+ dest .Network = net .Network_UDP
63+ }
5164 sockopt := streamSettings .SocketSettings
5265
5366 if client , found := globalDialerMap [dialerConf {dest , streamSettings }]; found {
5467 return client , nil
5568 }
5669
57- transport := & http2.Transport {
58- DialTLSContext : func (hctx context.Context , string , addr string , tlsConfig * gotls.Config ) (net.Conn , error ) {
59- rawHost , rawPort , err := net .SplitHostPort (addr )
60- if err != nil {
61- return nil , err
62- }
63- if len (rawPort ) == 0 {
64- rawPort = "443"
65- }
66- port , err := net .PortFromString (rawPort )
67- if err != nil {
68- return nil , err
69- }
70- address := net .ParseAddress (rawHost )
70+ var transport http.RoundTripper
71+ if isH3 {
72+ quicConfig := & quic.Config {
73+ MaxIdleTimeout : connIdleTimeout ,
7174
72- hctx = c .ContextWithID (hctx , c .IDFromContext (ctx ))
73- hctx = session .ContextWithOutbounds (hctx , session .OutboundsFromContext (ctx ))
74- hctx = session .ContextWithTimeoutOnly (hctx , true )
75+ // these two are defaults of quic-go/http3. the default of quic-go (no
76+ // http3) is different, so it is hardcoded here for clarity.
77+ // https://github.com/quic-go/quic-go/blob/b8ea5c798155950fb5bbfdd06cad1939c9355878/http3/client.go#L36-L39
78+ MaxIncomingStreams : - 1 ,
79+ KeepAlivePeriod : h3KeepalivePeriod ,
80+ }
81+ roundTripper := & http3.RoundTripper {
82+ QUICConfig : quicConfig ,
83+ TLSClientConfig : tlsConfigs .GetTLSConfig (tls .WithDestination (dest )),
84+ Dial : func (ctx context.Context , addr string , tlsCfg * gotls.Config , cfg * quic.Config ) (quic.EarlyConnection , error ) {
85+ conn , err := internet .DialSystem (ctx , dest , streamSettings .SocketSettings )
86+ if err != nil {
87+ return nil , err
88+ }
7589
76- pconn , err := internet .DialSystem (hctx , net .TCPDestination (address , port ), sockopt )
77- if err != nil {
78- errors .LogErrorInner (ctx , err , "failed to dial to " + addr )
79- return nil , err
80- }
90+ var udpConn net.PacketConn
91+ var udpAddr * net.UDPAddr
8192
82- if realityConfigs != nil {
83- return reality .UClient (pconn , realityConfigs , hctx , dest )
84- }
93+ switch c := conn .(type ) {
94+ case * internet.PacketConnWrapper :
95+ var ok bool
96+ udpConn , ok = c .Conn .(* net.UDPConn )
97+ if ! ok {
98+ return nil , errors .New ("PacketConnWrapper does not contain a UDP connection" )
99+ }
100+ udpAddr , err = net .ResolveUDPAddr ("udp" , c .Dest .String ())
101+ if err != nil {
102+ return nil , err
103+ }
104+ case * net.UDPConn :
105+ udpConn = c
106+ udpAddr , err = net .ResolveUDPAddr ("udp" , c .RemoteAddr ().String ())
107+ if err != nil {
108+ return nil , err
109+ }
110+ default :
111+ udpConn = & internet.FakePacketConn {c }
112+ udpAddr , err = net .ResolveUDPAddr ("udp" , c .RemoteAddr ().String ())
113+ if err != nil {
114+ return nil , err
115+ }
116+ }
85117
86- var cn tls.Interface
87- if fingerprint := tls .GetFingerprint (tlsConfigs .Fingerprint ); fingerprint != nil {
88- cn = tls .UClient (pconn , tlsConfig , fingerprint ).(* tls.UConn )
89- } else {
90- cn = tls .Client (pconn , tlsConfig ).(* tls.Conn )
91- }
92- if err := cn .HandshakeContext (ctx ); err != nil {
93- errors .LogErrorInner (ctx , err , "failed to dial to " + addr )
94- return nil , err
95- }
96- if ! tlsConfig .InsecureSkipVerify {
97- if err := cn .VerifyHostname (tlsConfig .ServerName ); err != nil {
118+ return quic .DialEarly (ctx , udpConn , udpAddr , tlsCfg , cfg )
119+ },
120+ }
121+ transport = roundTripper
122+ } else {
123+ transportH2 := & http2.Transport {
124+ DialTLSContext : func (hctx context.Context , string , addr string , tlsConfig * gotls.Config ) (net.Conn , error ) {
125+ rawHost , rawPort , err := net .SplitHostPort (addr )
126+ if err != nil {
127+ return nil , err
128+ }
129+ if len (rawPort ) == 0 {
130+ rawPort = "443"
131+ }
132+ port , err := net .PortFromString (rawPort )
133+ if err != nil {
134+ return nil , err
135+ }
136+ address := net .ParseAddress (rawHost )
137+
138+ hctx = c .ContextWithID (hctx , c .IDFromContext (ctx ))
139+ hctx = session .ContextWithOutbounds (hctx , session .OutboundsFromContext (ctx ))
140+ hctx = session .ContextWithTimeoutOnly (hctx , true )
141+
142+ pconn , err := internet .DialSystem (hctx , net .TCPDestination (address , port ), sockopt )
143+ if err != nil {
98144 errors .LogErrorInner (ctx , err , "failed to dial to " + addr )
99145 return nil , err
100146 }
101- }
102- negotiatedProtocol := cn .NegotiatedProtocol ()
103- if negotiatedProtocol != http2 .NextProtoTLS {
104- return nil , errors .New ("http2: unexpected ALPN protocol " + negotiatedProtocol + "; want q" + http2 .NextProtoTLS ).AtError ()
105- }
106- return cn , nil
107- },
108- }
109-
110- if tlsConfigs != nil {
111- transport .TLSClientConfig = tlsConfigs .GetTLSConfig (tls .WithDestination (dest ))
112- }
113-
114- if httpSettings .IdleTimeout > 0 || httpSettings .HealthCheckTimeout > 0 {
115- transport .ReadIdleTimeout = time .Second * time .Duration (httpSettings .IdleTimeout )
116- transport .PingTimeout = time .Second * time .Duration (httpSettings .HealthCheckTimeout )
147+
148+ if realityConfigs != nil {
149+ return reality .UClient (pconn , realityConfigs , hctx , dest )
150+ }
151+
152+ var cn tls.Interface
153+ if fingerprint := tls .GetFingerprint (tlsConfigs .Fingerprint ); fingerprint != nil {
154+ cn = tls .UClient (pconn , tlsConfig , fingerprint ).(* tls.UConn )
155+ } else {
156+ cn = tls .Client (pconn , tlsConfig ).(* tls.Conn )
157+ }
158+ if err := cn .HandshakeContext (ctx ); err != nil {
159+ errors .LogErrorInner (ctx , err , "failed to dial to " + addr )
160+ return nil , err
161+ }
162+ if ! tlsConfig .InsecureSkipVerify {
163+ if err := cn .VerifyHostname (tlsConfig .ServerName ); err != nil {
164+ errors .LogErrorInner (ctx , err , "failed to dial to " + addr )
165+ return nil , err
166+ }
167+ }
168+ negotiatedProtocol := cn .NegotiatedProtocol ()
169+ if negotiatedProtocol != http2 .NextProtoTLS {
170+ return nil , errors .New ("http2: unexpected ALPN protocol " + negotiatedProtocol + "; want q" + http2 .NextProtoTLS ).AtError ()
171+ }
172+ return cn , nil
173+ },
174+ }
175+ if tlsConfigs != nil {
176+ transportH2 .TLSClientConfig = tlsConfigs .GetTLSConfig (tls .WithDestination (dest ))
177+ }
178+ if httpSettings .IdleTimeout > 0 || httpSettings .HealthCheckTimeout > 0 {
179+ transportH2 .ReadIdleTimeout = time .Second * time .Duration (httpSettings .IdleTimeout )
180+ transportH2 .PingTimeout = time .Second * time .Duration (httpSettings .HealthCheckTimeout )
181+ }
182+ transport = transportH2
117183 }
118184
119185 client := & http.Client {
@@ -158,9 +224,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
158224 Host : dest .NetAddr (),
159225 Path : httpSettings .getNormalizedPath (),
160226 },
161- Proto : "HTTP/2" ,
162- ProtoMajor : 2 ,
163- ProtoMinor : 0 ,
164227 Header : httpHeaders ,
165228 }
166229 // Disable any compression method from server.
0 commit comments