mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-21 16:54:29 +00:00
https://github.com/XTLS/Xray-core/pull/7181#issuecomment-6229694351 https://github.com/XTLS/Xray-core/pull/7187#issuecomment-6250200781 https://github.com/XTLS/Xray-core/pull/7187#issuecomment-6314784068
483 lines
14 KiB
Go
483 lines
14 KiB
Go
package xdrive
|
|
|
|
import (
|
|
"context"
|
|
gotls "crypto/tls"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"runtime"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/apernet/quic-go"
|
|
"github.com/apernet/quic-go/http3"
|
|
"github.com/xtls/xray-core/common/errors"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/net/cnc"
|
|
"github.com/xtls/xray-core/common/utils"
|
|
"github.com/xtls/xray-core/transport/internet"
|
|
"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/splithttp"
|
|
"github.com/xtls/xray-core/transport/internet/tls"
|
|
)
|
|
|
|
var errStalled = errors.New("the request made no progress")
|
|
|
|
type serviceTransport struct {
|
|
plain http.RoundTripper
|
|
secure http.RoundTripper
|
|
stall time.Duration
|
|
}
|
|
|
|
// RoundTrip ends a request that stands still for t.stall, with no answer that
|
|
// long after its body went out or no byte of either body, but not one that just
|
|
// runs long: a large object on a slow link takes minutes and is fine as long as
|
|
// its bytes move
|
|
func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
next := t.plain
|
|
if r.URL.Scheme == "https" {
|
|
next = t.secure
|
|
}
|
|
|
|
ctx, cancel := context.WithCancelCause(r.Context())
|
|
timer := time.AfterFunc(t.stall, func() { cancel(errStalled) })
|
|
alive := func() { timer.Reset(t.stall) }
|
|
end := func() {
|
|
timer.Stop()
|
|
cancel(nil)
|
|
}
|
|
r = r.WithContext(ctx)
|
|
// the headers of chrome as with XHTTP and DoH, net/http and http3 would send
|
|
// their own user agent after a hello of chrome
|
|
r.Header = r.Header.Clone()
|
|
utils.TryDefaultHeadersWith(r.Header, "fetch")
|
|
if r.Body != nil && r.Body != http.NoBody {
|
|
r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
|
|
}
|
|
// a body sent again by the transport itself, after a GOAWAY or on a reused
|
|
// connection that failed, moves just the same
|
|
if getBody := r.GetBody; getBody != nil {
|
|
r.GetBody = func() (io.ReadCloser, error) {
|
|
body, err := getBody()
|
|
if err != nil || body == http.NoBody {
|
|
return body, err
|
|
}
|
|
return &progressBody{ReadCloser: body, alive: alive}, nil
|
|
}
|
|
}
|
|
|
|
resp, err := next.RoundTrip(r)
|
|
if err != nil {
|
|
err = stalled(ctx, err)
|
|
end()
|
|
return nil, err
|
|
}
|
|
alive()
|
|
resp.Body = &progressBody{ReadCloser: resp.Body, alive: alive, end: end, ctx: ctx}
|
|
return resp, nil
|
|
}
|
|
|
|
// stalled returns errStalled for an error that the stall gave, which http2 reports
|
|
// as a plain cancel
|
|
func stalled(ctx context.Context, err error) error {
|
|
if context.Cause(ctx) == errStalled {
|
|
return errStalled
|
|
}
|
|
return err
|
|
}
|
|
|
|
type progressBody struct {
|
|
io.ReadCloser
|
|
alive func()
|
|
end func()
|
|
// ctx is that of the request, for the body of its response
|
|
ctx context.Context
|
|
}
|
|
|
|
func (b *progressBody) Read(p []byte) (int, error) {
|
|
// http2 reads a request body up to 512 KiB at a time and sends it all before
|
|
// the next read, which on a slow link could look like no progress
|
|
n, err := b.ReadCloser.Read(p[:min(len(p), 16<<10)])
|
|
if n > 0 {
|
|
b.alive()
|
|
}
|
|
if err != nil && err != io.EOF && b.ctx != nil {
|
|
err = stalled(b.ctx, err)
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (b *progressBody) Close() error {
|
|
err := b.ReadCloser.Close()
|
|
if b.end != nil {
|
|
b.end()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// newServiceClient returns a client whose requests end after stall without
|
|
// progress, see serviceTransport.RoundTrip, and whose connections close when
|
|
// connCtx is done. Clients that share h1Hosts share which hosts answer over h1
|
|
func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
|
|
var (
|
|
tlsConfig *tls.Config
|
|
sockopt *internet.SocketConfig
|
|
fronting *net.Destination
|
|
keepAlive = net.ChromeH2KeepAlivePeriod
|
|
// h3 keeps alive only when asked to, see below
|
|
h3KeepAlive time.Duration
|
|
)
|
|
if streamSettings != nil {
|
|
tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
|
|
sockopt = streamSettings.SocketSettings
|
|
fronting = streamSettings.Destination
|
|
if config, ok := streamSettings.ProtocolSettings.(*Config); ok && config.Xmux != nil {
|
|
switch period := config.Xmux.HKeepAlivePeriod; {
|
|
case period > 0:
|
|
keepAlive = time.Duration(period) * time.Second
|
|
h3KeepAlive = keepAlive
|
|
case period < 0:
|
|
keepAlive = 0
|
|
}
|
|
}
|
|
}
|
|
if tlsConfig == nil {
|
|
tlsConfig = &tls.Config{}
|
|
}
|
|
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
|
|
|
|
// fronting takes only the address and port, the network stays that of the
|
|
// dial, which is UDP for h3 though the destination is built as TCP
|
|
dial := func(ctx context.Context, network, addr string) (net.Conn, net.Destination, error) {
|
|
host, err := net.ParseDestination(network + ":" + addr)
|
|
if err != nil {
|
|
return nil, host, errors.New("bad address: ", addr).Base(err)
|
|
}
|
|
|
|
target := host
|
|
if fronting != nil {
|
|
target.Address = fronting.Address
|
|
if fronting.Port != 0 {
|
|
target.Port = fronting.Port
|
|
}
|
|
}
|
|
|
|
var conn net.Conn
|
|
switch {
|
|
case streamSettings.FinalMask == nil:
|
|
conn, err = internet.DialSystem(ctx, target, sockopt)
|
|
case target.Network == net.Network_UDP:
|
|
conn, err = streamSettings.FinalMask.DialUDP(ctx, target)
|
|
default:
|
|
conn, err = streamSettings.FinalMask.DialTCP(ctx, target)
|
|
}
|
|
if err != nil {
|
|
return nil, host, errors.New("failed to dial to dest").Base(err)
|
|
}
|
|
// h3 closes its QUIC connection instead, as the server never hears of a UDP
|
|
// socket that closes
|
|
if network == "udp" {
|
|
return conn, host, nil
|
|
}
|
|
// CloseIdleConnections leaves an h2 connection open while a stream just
|
|
// canceled has not left it yet
|
|
stop := context.AfterFunc(connCtx, func() { conn.Close() })
|
|
return &ctxConn{Conn: conn, stop: stop}, host, nil
|
|
}
|
|
|
|
dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
conn, _, err := dial(ctx, "tcp", addr)
|
|
return conn, err
|
|
}
|
|
|
|
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
|
|
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
conn, host, err := dial(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
|
var tlsConn net.Conn
|
|
if fingerprint == nil {
|
|
tlsConn = tls.Client(conn, gotlsConfig)
|
|
} else {
|
|
tlsConn = tls.UClient(conn, gotlsConfig, fingerprint)
|
|
}
|
|
// the dial outlives the request that started it, and a handshake that hangs
|
|
// would keep the one connection of the host to itself
|
|
ctx, cancel := context.WithTimeout(ctx, stall)
|
|
defer cancel()
|
|
if err := tlsConn.(tls.Interface).HandshakeContext(ctx); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
return tlsConn, nil
|
|
}
|
|
|
|
secure := func(connsPerHost int) *http.Transport {
|
|
return &http.Transport{
|
|
DialTLSContext: dialTLS,
|
|
ForceAttemptHTTP2: true,
|
|
IdleConnTimeout: net.ConnIdleTimeout,
|
|
MaxIdleConns: maxConns,
|
|
MaxIdleConnsPerHost: maxConns,
|
|
MaxConnsPerHost: connsPerHost,
|
|
HTTP2: &http.HTTP2Config{SendPingTimeout: keepAlive},
|
|
}
|
|
}
|
|
|
|
var https http.RoundTripper = &secureTransport{h2: secure(1), h1: secure(maxConns), h1Hosts: h1Hosts}
|
|
// no server offers h3 by ALPN over TCP, so it is used only when asked for alone
|
|
if slices.Equal(tlsConfig.NextProtocol, []string{"h3"}) {
|
|
quicParams := streamSettings.QuicParams
|
|
if quicParams == nil {
|
|
quicParams = &internet.QuicParams{BbrProfile: string(bbr.ProfileStandard)}
|
|
}
|
|
// the defaults are those of quic-go, with no keepalive unless asked for, as
|
|
// in DoH: http3 never closes a connection that is idle or that it dropped
|
|
// after an error, so only the idle timeout or the end of connCtx ends it
|
|
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,
|
|
MaxIncomingStreams: quicParams.MaxIncomingStreams,
|
|
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
|
ChromeParrot: !quicParams.DisableChromeParrot,
|
|
}
|
|
if quicConfig.KeepAlivePeriod == 0 {
|
|
quicConfig.KeepAlivePeriod = h3KeepAlive
|
|
}
|
|
|
|
https = &http3.Transport{
|
|
QUICConfig: quicConfig,
|
|
Dial: func(ctx context.Context, addr string, _ *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
|
switch quicParams.Congestion {
|
|
case "", "bbr", "reno":
|
|
default:
|
|
// brutal is for Hysteria servers, not services, and a panic here, in
|
|
// a goroutine of http3, would end the process
|
|
return nil, errors.New("unsupported congestion control over h3: ", quicParams.Congestion)
|
|
}
|
|
|
|
conn, host, err := dial(ctx, "udp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pktConn net.PacketConn
|
|
var udpAddr net.Addr
|
|
switch c := conn.(type) {
|
|
case *net.PacketConnWrapper:
|
|
pktConn = c.PacketConn
|
|
udpAddr = c.RemoteAddr()
|
|
case *cnc.Connection:
|
|
pktConn = &internet.FakePacketConn{Conn: c}
|
|
udpAddr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
|
|
default:
|
|
conn.Close()
|
|
return nil, errors.New("failed to dial to dest: unexpected ", reflect.TypeOf(c))
|
|
}
|
|
|
|
// built for each dial with the host as SNI, as for h2: http3 sets the
|
|
// SNI on a copy, which a pinned CA and the ECH lookup do not see
|
|
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
|
// it is for servers, and ChromeParrot refuses a config that has it
|
|
gotlsConfig.GetCertificate = nil
|
|
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
|
|
if !quicParams.DisableChromeParrot {
|
|
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
|
|
}
|
|
qconn, err := tr.Dial(ctx, udpAddr, gotlsConfig, cfg)
|
|
if err != nil {
|
|
tr.Close()
|
|
pktConn.Close()
|
|
return nil, err
|
|
}
|
|
stop := context.AfterFunc(connCtx, func() { qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "") })
|
|
context.AfterFunc(qconn.Context(), func() { stop(); tr.Close(); pktConn.Close() })
|
|
if quicParams.Congestion != "reno" {
|
|
congestion.UseBBR(qconn, bbr.Profile(quicParams.BbrProfile))
|
|
}
|
|
return qconn, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: &serviceTransport{
|
|
plain: &http.Transport{
|
|
DialContext: dialPlain,
|
|
IdleConnTimeout: net.ConnIdleTimeout,
|
|
MaxIdleConns: maxConns,
|
|
MaxIdleConnsPerHost: maxConns,
|
|
MaxConnsPerHost: maxConns,
|
|
},
|
|
secure: https,
|
|
stall: stall,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ctxConn is a connection that connCtx no longer closes once it is closed,
|
|
// so that one that ends on its own leaves nothing behind in connCtx
|
|
type ctxConn struct {
|
|
net.Conn
|
|
stop func() bool
|
|
}
|
|
|
|
func (c *ctxConn) Close() error {
|
|
c.stop()
|
|
return c.Conn.Close()
|
|
}
|
|
|
|
// secureTransport keeps a host to one connection, all that h2 needs, as without
|
|
// a limit parallel requests would each dial their own. A host that answers over
|
|
// h1 moves to a transport with more, since h1 carries one request at a time
|
|
type secureTransport struct {
|
|
h2, h1 http.RoundTripper
|
|
h1Hosts *sync.Map
|
|
}
|
|
|
|
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
if _, ok := t.h1Hosts.Load(r.URL.Host); ok {
|
|
return t.h1.RoundTrip(r)
|
|
}
|
|
resp, err := t.h2.RoundTrip(r)
|
|
if err == nil && resp.ProtoMajor == 1 {
|
|
t.h1Hosts.Store(r.URL.Host, true)
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
var errSessionClosed = errors.New("the session is closed")
|
|
|
|
// connPool hands the sessions of a storage the connections of XMUX, each a
|
|
// client of its own made by newClient
|
|
type connPool struct {
|
|
mu sync.Mutex
|
|
xmux *splithttp.XmuxManager
|
|
}
|
|
|
|
func newConnPool(config *splithttp.XmuxConfig, newClient func(connCtx context.Context) *http.Client) *connPool {
|
|
var xmuxConfig splithttp.XmuxConfig
|
|
if config != nil {
|
|
xmuxConfig = *config
|
|
}
|
|
return &connPool{xmux: splithttp.NewXmuxManager(xmuxConfig, func() splithttp.XmuxConn {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &poolConn{next: newClient(ctx).Transport, cancel: cancel}
|
|
})}
|
|
}
|
|
|
|
// poolConn is a connection of XMUX, which closes once XMUX drops it and the
|
|
// requests on it end
|
|
type poolConn struct {
|
|
next http.RoundTripper
|
|
cancel context.CancelFunc
|
|
|
|
mu sync.Mutex
|
|
inflight int
|
|
closing bool
|
|
}
|
|
|
|
func (c *poolConn) IsClosed() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.closing
|
|
}
|
|
|
|
func (c *poolConn) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.closing = true
|
|
if c.inflight == 0 {
|
|
c.cancel()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *poolConn) done() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.inflight--
|
|
if c.closing && c.inflight == 0 {
|
|
c.cancel()
|
|
}
|
|
}
|
|
|
|
// roundTrip sends a request counted in inflight by the caller
|
|
func (c *poolConn) roundTrip(r *http.Request) (*http.Response, error) {
|
|
resp, err := c.next.RoundTrip(r)
|
|
if err != nil {
|
|
c.done()
|
|
return nil, err
|
|
}
|
|
resp.Body = &doneBody{ReadCloser: resp.Body, done: sync.OnceFunc(c.done)}
|
|
return resp, nil
|
|
}
|
|
|
|
type doneBody struct {
|
|
io.ReadCloser
|
|
done func()
|
|
}
|
|
|
|
func (b *doneBody) Close() error {
|
|
err := b.ReadCloser.Close()
|
|
b.done()
|
|
return err
|
|
}
|
|
|
|
// sessionTransport sends the requests of one session over a connection of
|
|
// XMUX, and over another once that one has no requests or time left
|
|
type sessionTransport struct {
|
|
pool *connPool
|
|
|
|
mu sync.Mutex
|
|
client *splithttp.XmuxClient
|
|
closed bool
|
|
}
|
|
|
|
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return nil, errSessionClosed
|
|
}
|
|
client := t.client
|
|
if client == nil || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
|
|
t.pool.mu.Lock()
|
|
t.client = t.pool.xmux.GetXmuxClient(r.Context())
|
|
t.pool.mu.Unlock()
|
|
t.client.AddRunning()
|
|
t.client.LeftRequests.Add(-1)
|
|
if client != nil {
|
|
client.DoneRunning()
|
|
}
|
|
client = t.client
|
|
}
|
|
// counted under mu, or another request of the session that moves on could
|
|
// close the connection before this one is sent
|
|
conn := client.XmuxConn.(*poolConn)
|
|
conn.mu.Lock()
|
|
conn.inflight++
|
|
conn.mu.Unlock()
|
|
t.mu.Unlock()
|
|
return conn.roundTrip(r)
|
|
}
|
|
|
|
func (t *sessionTransport) Close() {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.closed = true
|
|
if t.client != nil {
|
|
t.client.DoneRunning()
|
|
t.client = nil
|
|
}
|
|
}
|