diff --git a/docs/config.yaml b/docs/config.yaml index 28fb4eb1..21b4c146 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -807,7 +807,7 @@ proxies: # socks5 udp: true tls: true network: xhttp - alpn: [h2] # 默认为h2,如果开启h3模式需要设置alpn: [h3] + alpn: [h2] # 默认仅支持h2,如果开启h3模式需要设置alpn: [h3],如果开启http1.1模式需要设置alpn: [http/1.1] # ech-opts: ... # reality-opts: ... # skip-cert-verify: false @@ -851,8 +851,7 @@ proxies: # socks5 # server: server # port: 443 # tls: true - # alpn: - # - h2 + # alpn: ... # ech-opts: ... # reality-opts: ... # skip-cert-verify: false diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index 6e0fbda2..a3e707a5 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -488,6 +488,42 @@ func TestInboundVless_XHTTP_Reality(t *testing.T) { } } +func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + outboundOptions := outbound.VlessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: "packet-up", + }, + } + return inboundOptions, outboundOptions + } + + t.Run("default", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + }) +} + func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { out.XHTTPOpts.ReuseSettings = &outbound.XHTTPReuseSettings{ MaxConnections: "0", diff --git a/transport/xhttp/client.go b/transport/xhttp/client.go index 42388237..09b4bbee 100644 --- a/transport/xhttp/client.go +++ b/transport/xhttp/client.go @@ -19,6 +19,7 @@ import ( "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" "github.com/metacubex/tls" + "golang.org/x/sync/semaphore" ) type DialRawFunc func(ctx context.Context) (net.Conn, error) @@ -100,6 +101,16 @@ func (c *PacketUpWriter) Close() error { return nil } +// h1Transport is a wrapper that forces the underlying transport to use HTTP/1.1. +type h1Transport struct { + http.RoundTripper +} + +func (c h1Transport) RoundTrip(req *http.Request) (*http.Response, error) { + req.URL.Scheme = "http" // change the scheme to http allow we can make TLS in ourselves Transport.DialContext + return c.RoundTripper.RoundTrip(req) +} + func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, dialQUIC DialQUICFunc, alpn []string) http.RoundTripper { if len(alpn) == 1 && alpn[0] == "h3" { // `alpn: [h3]` means using h3 mode return &http3.Transport{ @@ -112,6 +123,28 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc, dialQUIC DialQUICFun }, } } + if len(alpn) == 1 && alpn[0] == "http/1.1" { // `alpn: [http/1.1]` means using http/1.1 mode + w := semaphore.NewWeighted(20) // limit concurrent dialing to avoid WSAECONNREFUSED on Windows + return h1Transport{&http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if err := w.Acquire(ctx, 1); err != nil { + return nil, err + } + defer w.Release(1) + raw, err := dialRaw(ctx) + if err != nil { + return nil, err + } + wrapped, err := wrapTLS(ctx, raw, false) + if err != nil { + _ = raw.Close() + return nil, err + } + return wrapped, nil + }, + ForceAttemptHTTP2: false, // only http/1.1 + }} + } return &http.Http2Transport{ DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { raw, err := dialRaw(ctx)