Fix HTTP/2 extended CONNECT with Go 1.27

This commit is contained in:
世界 committed 2026-11-09 15:17:20 +00:00
1 parent bc6bb79518
commit edb9c62f72
12 files changed
+120 -49

No files matched your search

-35
View File
@@ -8,7 +8,6 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"testing"
"time"
@@ -118,40 +117,6 @@ func TestHTTPInboundConnectUDP(t *testing.T) {
}
}
func TestHTTPInboundConnectUDPHTTP2(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startTLSHTTPInbound(t, certPem, keyPem, nil, nil)
echo := startUDPEcho(t)
clientConn := dialHTTP2Proxy(t, serverPort)
pipeReader, pipeWriter := io.Pipe()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: "example.org",
Path: connectUDPPath(echo),
},
Host: "example.org",
Header: http.Header{
":protocol": []string{"connect-udp"},
"Capsule-Protocol": []string{"?1"},
"Proxy-Authorization": []string{proxyAuthorization},
},
Body: pipeReader,
}
response, err := clientConn.RoundTrip(request)
require.NoError(t, err)
defer response.Body.Close()
defer pipeWriter.Close()
require.Equal(t, http.StatusOK, response.StatusCode)
require.Equal(t, "?1", response.Header.Get("Capsule-Protocol"))
reader := std_bufio.NewReader(response.Body)
for i := 0; i < 3; i++ {
writeDatagramCapsule(t, pipeWriter, []byte("ping"))
require.Equal(t, "ping", string(readDatagramCapsule(t, reader)))
}
}
func testHTTPOutboundUDP(t *testing.T, tlsOptions *option.OutboundTLSOptions) {
startInstance(t, option.Options{
Inbounds: []option.Inbound{
+1
View File
@@ -363,6 +363,7 @@ var (
ErrHTTP2Unsupported = E.New("server does not support HTTP/2")
ErrHTTP3Unavailable = E.New("HTTP/3 unavailable")
errExtendedConnectUnsupported = E.New("server does not support HTTP/2 extended CONNECT")
errExtendedConnectUnavailable = E.New("HTTP/2 extended CONNECT is unavailable in this build: Go 1.27+ requires the badlinkname build tag")
)
func statusError(response *http.Response) error {
+5 -10
View File
@@ -16,16 +16,12 @@ import (
"golang.org/x/net/http2"
)
type http2ClientConn struct {
*http2.ClientConn
}
func (c *Client) acquireHTTP2(ctx context.Context) (*http2ClientConn, net.Conn, error) {
c.http2Access.Lock()
defer c.http2Access.Unlock()
c.http2Conns = slices.DeleteFunc(c.http2Conns, func(it *http2ClientConn) bool { return it.State().Closed })
c.http2Conns = slices.DeleteFunc(c.http2Conns, func(it *http2ClientConn) bool { return it.isClosed() })
for _, clientConn := range c.http2Conns {
if clientConn.ReserveNewRequest() {
if clientConn.reserveNewRequest() {
return clientConn, nil, nil
}
}
@@ -39,13 +35,12 @@ func (c *Client) acquireHTTP2(ctx context.Context) (*http2ClientConn, net.Conn,
}
return nil, conn, nil
}
rawClientConn, err := c.http2Transport.NewClientConn(conn)
clientConn, err := newHTTP2ClientConn(c.http2Transport, conn)
if err != nil {
conn.Close()
return nil, nil, E.Cause(err, "create HTTP/2 connection")
}
clientConn := &http2ClientConn{ClientConn: rawClientConn}
clientConn.ReserveNewRequest()
clientConn.reserveNewRequest()
c.http2Conns = append(c.http2Conns, clientConn)
return clientConn, nil, nil
}
@@ -70,7 +65,7 @@ func (c *Client) roundTripHTTP2(ctx context.Context, clientConn *http2ClientConn
request.Body = pipeReader
request = request.WithContext(streamCtx)
stop := context.AfterFunc(ctx, cancel)
response, err := clientConn.RoundTrip(request)
response, err := clientConn.roundTrip(request)
stopped := stop()
if err == nil && !stopped {
response.Body.Close()
+34
View File
@@ -0,0 +1,34 @@
//go:build !go1.27 || http2legacy || !badlinkname
package http
import (
"net"
"net/http"
"golang.org/x/net/http2"
)
type http2ClientConn struct {
*http2.ClientConn
}
func newHTTP2ClientConn(transport *http2.Transport, conn net.Conn) (*http2ClientConn, error) {
clientConn, err := transport.NewClientConn(conn)
if err != nil {
return nil, err
}
return &http2ClientConn{ClientConn: clientConn}, nil
}
func (c *http2ClientConn) isClosed() bool {
return c.State().Closed
}
func (c *http2ClientConn) reserveNewRequest() bool {
return c.ReserveNewRequest()
}
func (c *http2ClientConn) roundTrip(request *http.Request) (*http.Response, error) {
return c.RoundTrip(request)
}
+47
View File
@@ -0,0 +1,47 @@
//go:build go1.27 && !http2legacy && badlinkname
package http
import (
"net"
"net/http"
"unsafe"
"golang.org/x/net/http2"
)
type http2ClientConn struct {
*http.ClientConn
}
func newHTTP2ClientConn(transport *http2.Transport, conn net.Conn) (*http2ClientConn, error) {
clientConn, err := transport.NewClientConn(conn)
if err != nil {
return nil, err
}
return &http2ClientConn{ClientConn: *(**http.ClientConn)(unsafe.Pointer(clientConn))}, nil
}
func (c *http2ClientConn) isClosed() bool {
return c.Err() != nil
}
func (c *http2ClientConn) reserveNewRequest() bool {
return c.Reserve() == nil
}
func (c *http2ClientConn) roundTrip(request *http.Request) (*http.Response, error) {
if request.Header.Get(":protocol") == "" {
return c.ClientConn.RoundTrip(request)
}
internalClientConn := (*[2]unsafe.Pointer)(unsafe.Pointer(c.ClientConn))[1]
return http2RoundTrip(request, func(clientRequest unsafe.Pointer) (unsafe.Pointer, error) {
return netHTTPClientConnRoundTrip(internalClientConn, clientRequest)
})
}
//go:linkname http2RoundTrip net/http.http2RoundTrip
func http2RoundTrip(request *http.Request, roundTrip func(clientRequest unsafe.Pointer) (unsafe.Pointer, error)) (*http.Response, error)
//go:linkname netHTTPClientConnRoundTrip net/http/internal/http2.NetHTTPClientConn.RoundTrip
func netHTTPClientConnRoundTrip(clientConn unsafe.Pointer, clientRequest unsafe.Pointer) (unsafe.Pointer, error)
@@ -1,3 +1,5 @@
//go:build !go1.27 || http2legacy
package http
import (
@@ -6,6 +8,8 @@ import (
_ "golang.org/x/net/http2"
)
const extendedConnectAvailable = true
//go:linkname disableExtendedConnectProtocol golang.org/x/net/http2.disableExtendedConnectProtocol
var disableExtendedConnectProtocol bool
@@ -0,0 +1,17 @@
//go:build go1.27 && !http2legacy && badlinkname
package http
import (
_ "net/http"
_ "unsafe"
)
const extendedConnectAvailable = true
//go:linkname disableExtendedConnectProtocol net/http/internal/http2.disableExtendedConnectProtocol
var disableExtendedConnectProtocol bool
func init() {
disableExtendedConnectProtocol = false
}
@@ -0,0 +1,5 @@
//go:build go1.27 && !http2legacy && !badlinkname
package http
const extendedConnectAvailable = false
+4 -1
View File
@@ -56,7 +56,10 @@ func (c *Client) openTunnel(ctx context.Context, request tunnelRequest) (net.Con
}
c.markHTTP3Broken()
}
if c.tlsDialer != nil && !c.http2Unsupported.Load() && !c.http2ExtendedConnectUnsupported.Load() {
if !extendedConnectAvailable && c.tlsDialer != nil && c.disableVersionFallback {
return nil, nil, errExtendedConnectUnavailable
}
if extendedConnectAvailable && c.tlsDialer != nil && !c.http2Unsupported.Load() && !c.http2ExtendedConnectUnsupported.Load() {
clientConn, conn, err := c.acquireHTTP2(ctx)
if err != nil {
return nil, nil, err
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build go1.27 && badlinkname
//go:build go1.27 && !http2legacy && badlinkname
package v2rayhttp
@@ -1,4 +1,4 @@
//go:build go1.27 && !badlinkname
//go:build go1.27 && !http2legacy && !badlinkname
package v2rayhttp
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !go1.27
//go:build !go1.27 || http2legacy
package v2rayhttp