From d017cbe0087df16b6f640b50a40c1511b5850006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Mar 2026 13:09:09 +0800 Subject: [PATCH] Stabilize cloudflare edge transport fallback --- protocol/cloudflare/connection_http2.go | 5 +- protocol/cloudflare/connection_quic.go | 79 ++++++++++++++++++++++--- protocol/cloudflare/inbound.go | 7 ++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/protocol/cloudflare/connection_http2.go b/protocol/cloudflare/connection_http2.go index 42d9bffea..2de68d464 100644 --- a/protocol/cloudflare/connection_http2.go +++ b/protocol/cloudflare/connection_http2.go @@ -67,9 +67,8 @@ func NewHTTP2Connection( } tlsConfig := &tls.Config{ - RootCAs: rootCAs, - ServerName: h2EdgeSNI, - CurvePreferences: []tls.CurveID{tls.CurveP256}, + RootCAs: rootCAs, + ServerName: h2EdgeSNI, } dialer := &net.Dialer{} diff --git a/protocol/cloudflare/connection_quic.go b/protocol/cloudflare/connection_quic.go index 549a6eef4..f06ad714f 100644 --- a/protocol/cloudflare/connection_quic.go +++ b/protocol/cloudflare/connection_quic.go @@ -5,8 +5,10 @@ package cloudflare import ( "context" "crypto/tls" + "fmt" "io" "net" + "runtime" "sync" "time" @@ -36,7 +38,7 @@ func quicInitialPacketSize(ipVersion int) uint16 { // QUICConnection manages a single QUIC connection to the Cloudflare edge. type QUICConnection struct { - conn *quic.Conn + conn quicConnection logger log.ContextLogger edgeAddr *EdgeAddr connIndex uint8 @@ -51,6 +53,31 @@ type QUICConnection struct { closeOnce sync.Once } +type quicConnection interface { + OpenStream() (*quic.Stream, error) + AcceptStream(ctx context.Context) (*quic.Stream, error) + ReceiveDatagram(ctx context.Context) ([]byte, error) + SendDatagram(data []byte) error + LocalAddr() net.Addr + CloseWithError(code quic.ApplicationErrorCode, reason string) error +} + +type closeableQUICConn struct { + *quic.Conn + udpConn *net.UDPConn +} + +func (c *closeableQUICConn) CloseWithError(code quic.ApplicationErrorCode, reason string) error { + err := c.Conn.CloseWithError(code, reason) + _ = c.udpConn.Close() + return err +} + +var ( + quicPortByConnIndex = make(map[uint8]int) + quicPortAccess sync.Mutex +) + // NewQUICConnection dials the edge and establishes a QUIC connection. func NewQUICConnection( ctx context.Context, @@ -69,10 +96,9 @@ func NewQUICConnection( } tlsConfig := &tls.Config{ - RootCAs: rootCAs, - ServerName: quicEdgeSNI, - NextProtos: []string{quicEdgeALPN}, - CurvePreferences: []tls.CurveID{tls.CurveP256}, + RootCAs: rootCAs, + ServerName: quicEdgeSNI, + NextProtos: []string{quicEdgeALPN}, } quicConfig := &quic.Config{ @@ -85,13 +111,19 @@ func NewQUICConnection( InitialPacketSize: quicInitialPacketSize(edgeAddr.IPVersion), } - conn, err := quic.DialAddr(ctx, edgeAddr.UDP.String(), tlsConfig, quicConfig) + udpConn, err := createUDPConnForConnIndex(connIndex, edgeAddr) if err != nil { + return nil, E.Cause(err, "listen UDP for QUIC edge") + } + + conn, err := quic.Dial(ctx, udpConn, edgeAddr.UDP, tlsConfig, quicConfig) + if err != nil { + udpConn.Close() return nil, E.Cause(err, "dial QUIC edge") } return &QUICConnection{ - conn: conn, + conn: &closeableQUICConn{Conn: conn, udpConn: udpConn}, logger: logger, edgeAddr: edgeAddr, connIndex: connIndex, @@ -103,6 +135,39 @@ func NewQUICConnection( }, nil } +func createUDPConnForConnIndex(connIndex uint8, edgeAddr *EdgeAddr) (*net.UDPConn, error) { + quicPortAccess.Lock() + defer quicPortAccess.Unlock() + + network := "udp" + if runtime.GOOS == "darwin" { + if edgeAddr.IPVersion == 4 { + network = "udp4" + } else { + network = "udp6" + } + } + + if port, loaded := quicPortByConnIndex[connIndex]; loaded { + udpConn, err := net.ListenUDP(network, &net.UDPAddr{Port: port}) + if err == nil { + return udpConn, nil + } + } + + udpConn, err := net.ListenUDP(network, &net.UDPAddr{Port: 0}) + if err != nil { + return nil, err + } + udpAddr, ok := udpConn.LocalAddr().(*net.UDPAddr) + if !ok { + udpConn.Close() + return nil, fmt.Errorf("unexpected local UDP address type %T", udpConn.LocalAddr()) + } + quicPortByConnIndex[connIndex] = udpAddr.Port + return udpConn, nil +} + // Serve runs the QUIC connection: registers, accepts streams, handles datagrams. // Blocks until the context is cancelled or a fatal error occurs. func (q *QUICConnection) Serve(ctx context.Context, handler StreamHandler) error { diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go index e7cbdb4b6..ae48ba40e 100644 --- a/protocol/cloudflare/inbound.go +++ b/protocol/cloudflare/inbound.go @@ -268,7 +268,12 @@ func (i *Inbound) serveConnection(connIndex uint8, edgeAddr *EdgeAddr, features switch protocol { case "quic": - return i.serveQUIC(connIndex, edgeAddr, features, numPreviousAttempts) + err := i.serveQUIC(connIndex, edgeAddr, features, numPreviousAttempts) + if err == nil || i.ctx.Err() != nil { + return err + } + i.logger.Warn("QUIC connection failed, falling back to HTTP/2: ", err) + return i.serveHTTP2(connIndex, edgeAddr, features, numPreviousAttempts) case "http2": return i.serveHTTP2(connIndex, edgeAddr, features, numPreviousAttempts) default: