Sfoglia il codice sorgente

Add ECH support for QUIC based protocols

世界 2 anni fa
parent
commit
4c050d7f4b

+ 120 - 0
common/qtls/wrapper.go

@@ -0,0 +1,120 @@
+package qtls
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"net/http"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/quic-go/http3"
+	M "github.com/sagernet/sing/common/metadata"
+	aTLS "github.com/sagernet/sing/common/tls"
+)
+
+type QUICConfig interface {
+	Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error)
+	DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error)
+	CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper
+}
+
+type QUICServerConfig interface {
+	Listen(conn net.PacketConn, config *quic.Config) (QUICListener, error)
+	ListenEarly(conn net.PacketConn, config *quic.Config) (QUICEarlyListener, error)
+	ConfigureHTTP3()
+}
+
+type QUICListener interface {
+	Accept(ctx context.Context) (quic.Connection, error)
+	Close() error
+	Addr() net.Addr
+}
+
+type QUICEarlyListener interface {
+	Accept(ctx context.Context) (quic.EarlyConnection, error)
+	Close() error
+	Addr() net.Addr
+}
+
+func Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config aTLS.Config, quicConfig *quic.Config) (quic.Connection, error) {
+	if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
+		return quicTLSConfig.Dial(ctx, conn, addr, quicConfig)
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return nil, err
+	}
+	return quic.Dial(ctx, conn, addr, tlsConfig, quicConfig)
+}
+
+func DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config aTLS.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
+	if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
+		return quicTLSConfig.DialEarly(ctx, conn, addr, quicConfig)
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return nil, err
+	}
+	return quic.DialEarly(ctx, conn, addr, tlsConfig, quicConfig)
+}
+
+func CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, config aTLS.Config, quicConfig *quic.Config, enableDatagrams bool) (http.RoundTripper, error) {
+	if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
+		return quicTLSConfig.CreateTransport(conn, quicConnPtr, serverAddr, quicConfig, enableDatagrams), nil
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return nil, err
+	}
+	return &http3.RoundTripper{
+		TLSClientConfig: tlsConfig,
+		QuicConfig:      quicConfig,
+		EnableDatagrams: enableDatagrams,
+		Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+			quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
+			if err != nil {
+				return nil, err
+			}
+			*quicConnPtr = quicConn
+			return quicConn, nil
+		},
+	}, nil
+}
+
+func Listen(conn net.PacketConn, config aTLS.ServerConfig, quicConfig *quic.Config) (QUICListener, error) {
+	if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
+		return quicTLSConfig.Listen(conn, quicConfig)
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return nil, err
+	}
+	return quic.Listen(conn, tlsConfig, quicConfig)
+}
+
+func ListenEarly(conn net.PacketConn, config aTLS.ServerConfig, quicConfig *quic.Config) (QUICEarlyListener, error) {
+	if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
+		return quicTLSConfig.ListenEarly(conn, quicConfig)
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return nil, err
+	}
+	return quic.ListenEarly(conn, tlsConfig, quicConfig)
+}
+
+func ConfigureHTTP3(config aTLS.ServerConfig) error {
+	if len(config.NextProtos()) == 0 {
+		config.SetNextProtos([]string{http3.NextProtoH3})
+	}
+	if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
+		quicTLSConfig.ConfigureHTTP3()
+		return nil
+	}
+	tlsConfig, err := config.Config()
+	if err != nil {
+		return err
+	}
+	http3.ConfigureTLSConfig(tlsConfig)
+	return nil
+}

+ 29 - 17
common/tls/ech_client.go

@@ -23,37 +23,37 @@ import (
 	mDNS "github.com/miekg/dns"
 )
 
-type ECHClientConfig struct {
+type echClientConfig struct {
 	config *cftls.Config
 }
 
-func (e *ECHClientConfig) ServerName() string {
-	return e.config.ServerName
+func (c *echClientConfig) ServerName() string {
+	return c.config.ServerName
 }
 
-func (e *ECHClientConfig) SetServerName(serverName string) {
-	e.config.ServerName = serverName
+func (c *echClientConfig) SetServerName(serverName string) {
+	c.config.ServerName = serverName
 }
 
-func (e *ECHClientConfig) NextProtos() []string {
-	return e.config.NextProtos
+func (c *echClientConfig) NextProtos() []string {
+	return c.config.NextProtos
 }
 
-func (e *ECHClientConfig) SetNextProtos(nextProto []string) {
-	e.config.NextProtos = nextProto
+func (c *echClientConfig) SetNextProtos(nextProto []string) {
+	c.config.NextProtos = nextProto
 }
 
-func (e *ECHClientConfig) Config() (*STDConfig, error) {
+func (c *echClientConfig) Config() (*STDConfig, error) {
 	return nil, E.New("unsupported usage for ECH")
 }
 
-func (e *ECHClientConfig) Client(conn net.Conn) (Conn, error) {
-	return &echConnWrapper{cftls.Client(conn, e.config)}, nil
+func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
+	return &echConnWrapper{cftls.Client(conn, c.config)}, nil
 }
 
-func (e *ECHClientConfig) Clone() Config {
-	return &ECHClientConfig{
-		config: e.config.Clone(),
+func (c *echClientConfig) Clone() Config {
+	return &echClientConfig{
+		config: c.config.Clone(),
 	}
 }
 
@@ -171,8 +171,20 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
 	tlsConfig.ECHEnabled = true
 	tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
 	tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
+
+	var echConfig []byte
 	if len(options.ECH.Config) > 0 {
-		block, rest := pem.Decode([]byte(strings.Join(options.ECH.Config, "\n")))
+		echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
+	} else if options.ECH.ConfigPath != "" {
+		content, err := os.ReadFile(options.ECH.ConfigPath)
+		if err != nil {
+			return nil, E.Cause(err, "read ECH config")
+		}
+		echConfig = content
+	}
+
+	if len(echConfig) > 0 {
+		block, rest := pem.Decode(echConfig)
 		if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
 			return nil, E.New("invalid ECH configs pem")
 		}
@@ -184,7 +196,7 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
 	} else {
 		tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
 	}
-	return &ECHClientConfig{&tlsConfig}, nil
+	return &echClientConfig{&tlsConfig}, nil
 }
 
 func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {

+ 56 - 0
common/tls/ech_quic.go

@@ -0,0 +1,56 @@
+//go:build with_quic && with_ech
+
+package tls
+
+import (
+	"context"
+	"net"
+	"net/http"
+
+	"github.com/sagernet/cloudflare-tls"
+	"github.com/sagernet/quic-go/ech"
+	"github.com/sagernet/quic-go/http3_ech"
+	"github.com/sagernet/sing-box/common/qtls"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+var (
+	_ qtls.QUICConfig       = (*echClientConfig)(nil)
+	_ qtls.QUICServerConfig = (*echServerConfig)(nil)
+)
+
+func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
+	return quic.Dial(ctx, conn, addr, c.config, config)
+}
+
+func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
+	return quic.DialEarly(ctx, conn, addr, c.config, config)
+}
+
+func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper {
+	return &http3.RoundTripper{
+		TLSClientConfig: c.config,
+		QuicConfig:      quicConfig,
+		EnableDatagrams: enableDatagrams,
+		Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+			quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
+			if err != nil {
+				return nil, err
+			}
+			*quicConnPtr = quicConn
+			return quicConn, nil
+		},
+	}
+}
+
+func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.QUICListener, error) {
+	return quic.Listen(conn, c.config, config)
+}
+
+func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.QUICEarlyListener, error) {
+	return quic.ListenEarly(conn, c.config, config)
+}
+
+func (c *echServerConfig) ConfigureHTTP3() {
+	http3.ConfigureTLSConfig(c.config)
+}

+ 17 - 4
common/tls/ech_server.go

@@ -159,7 +159,7 @@ func (c *echServerConfig) startECHWatcher() error {
 	if err != nil {
 		return err
 	}
-	c.watcher = watcher
+	c.echWatcher = watcher
 	go c.loopECHUpdate()
 	return nil
 }
@@ -178,7 +178,7 @@ func (c *echServerConfig) loopECHUpdate() {
 			if err != nil {
 				c.logger.Error(E.Cause(err, "reload ECH key"))
 			}
-		case err, ok := <-c.watcher.Errors:
+		case err, ok := <-c.echWatcher.Errors:
 			if !ok {
 				return
 			}
@@ -277,7 +277,7 @@ func NewECHServer(ctx context.Context, logger log.Logger, options option.Inbound
 		certificate = content
 	}
 	if len(options.Key) > 0 {
-		key = []byte(strings.Join(options.Key, ""))
+		key = []byte(strings.Join(options.Key, "\n"))
 	} else if options.KeyPath != "" {
 		content, err := os.ReadFile(options.KeyPath)
 		if err != nil {
@@ -298,7 +298,20 @@ func NewECHServer(ctx context.Context, logger log.Logger, options option.Inbound
 	}
 	tlsConfig.Certificates = []cftls.Certificate{keyPair}
 
-	block, rest := pem.Decode([]byte(strings.Join(options.ECH.Key, "\n")))
+	var echKey []byte
+	if len(options.ECH.Key) > 0 {
+		echKey = []byte(strings.Join(options.ECH.Key, "\n"))
+	} else if options.KeyPath != "" {
+		content, err := os.ReadFile(options.ECH.KeyPath)
+		if err != nil {
+			return nil, E.Cause(err, "read ECH key")
+		}
+		echKey = content
+	} else {
+		return nil, E.New("missing ECH key")
+	}
+
+	block, rest := pem.Decode(echKey)
 	if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
 		return nil, E.New("invalid ECH keys pem")
 	}

+ 7 - 1
docs/configuration/shared/tls.md

@@ -188,6 +188,12 @@ The server private key line array, in PEM format.
 
 The path to the server private key, in PEM format.
 
+## Custom TLS support
+
+!!! info "QUIC support"
+
+    Only ECH is supported in QUIC.
+
 #### utls
 
 ==Client only==
@@ -217,7 +223,7 @@ Available fingerprint values:
 
 Chrome fingerprint will be used if empty.
 
-## ECH Fields
+### ECH Fields
 
 !!! warning ""
 

+ 4 - 7
inbound/hysteria.go

@@ -9,6 +9,7 @@ import (
 	"github.com/sagernet/quic-go"
 	"github.com/sagernet/quic-go/congestion"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/qtls"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -35,7 +36,7 @@ type Hysteria struct {
 	xplusKey     []byte
 	sendBPS      uint64
 	recvBPS      uint64
-	listener     *quic.Listener
+	listener     qtls.QUICListener
 	udpAccess    sync.RWMutex
 	udpSessionId uint32
 	udpSessions  map[uint32]chan *hysteria.UDPMessage
@@ -147,11 +148,7 @@ func (h *Hysteria) Start() error {
 	if err != nil {
 		return err
 	}
-	rawConfig, err := h.tlsConfig.Config()
-	if err != nil {
-		return err
-	}
-	listener, err := quic.Listen(packetConn, rawConfig, h.quicConfig)
+	listener, err := qtls.Listen(packetConn, h.tlsConfig, h.quicConfig)
 	if err != nil {
 		return err
 	}
@@ -333,7 +330,7 @@ func (h *Hysteria) Close() error {
 	h.udpAccess.Unlock()
 	return common.Close(
 		&h.myInboundAdapter,
-		common.PtrOrNil(h.listener),
+		h.listener,
 		h.tlsConfig,
 	)
 }

+ 18 - 7
inbound/naive_quic.go

@@ -3,28 +3,39 @@
 package inbound
 
 import (
+	"github.com/sagernet/quic-go"
 	"github.com/sagernet/quic-go/http3"
+	"github.com/sagernet/sing-box/common/qtls"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
 func (n *Naive) configureHTTP3Listener() error {
-	tlsConfig, err := n.tlsConfig.Config()
+	err := qtls.ConfigureHTTP3(n.tlsConfig)
 	if err != nil {
 		return err
 	}
-	h3Server := &http3.Server{
-		Port:      int(n.listenOptions.ListenPort),
-		TLSConfig: tlsConfig,
-		Handler:   n,
-	}
 
 	udpConn, err := n.ListenUDP()
 	if err != nil {
 		return err
 	}
 
+	quicListener, err := qtls.ListenEarly(udpConn, n.tlsConfig, &quic.Config{
+		MaxIncomingStreams: 1 << 60,
+		Allow0RTT:          true,
+	})
+	if err != nil {
+		udpConn.Close()
+		return err
+	}
+
+	h3Server := &http3.Server{
+		Port:    int(n.listenOptions.ListenPort),
+		Handler: n,
+	}
+
 	go func() {
-		sErr := h3Server.Serve(udpConn)
+		sErr := h3Server.ServeListener(quicListener)
 		udpConn.Close()
 		if sErr != nil && !E.IsClosedOrCanceled(sErr) {
 			n.logger.Error("http3 server serve error: ", sErr)

+ 1 - 5
inbound/tuic.go

@@ -38,10 +38,6 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	if err != nil {
 		return nil, err
 	}
-	rawConfig, err := tlsConfig.Config()
-	if err != nil {
-		return nil, err
-	}
 	var users []tuic.User
 	for index, user := range options.Users {
 		if user.UUID == "" {
@@ -67,7 +63,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	server, err := tuic.NewServer(tuic.ServerOptions{
 		Context:           ctx,
 		Logger:            logger,
-		TLSConfig:         rawConfig,
+		TLSConfig:         tlsConfig,
 		Users:             users,
 		CongestionControl: options.CongestionControl,
 		AuthTimeout:       time.Duration(options.AuthTimeout),

+ 2 - 2
option/tls.go

@@ -50,8 +50,8 @@ type InboundECHOptions struct {
 	Enabled                     bool             `json:"enabled,omitempty"`
 	PQSignatureSchemesEnabled   bool             `json:"pq_signature_schemes_enabled,omitempty"`
 	DynamicRecordSizingDisabled bool             `json:"dynamic_record_sizing_disabled,omitempty"`
-	Key                         Listable[string] `json:"ech_keys,omitempty"`
-	KeyPath                     string           `json:"ech_keys_path,omitempty"`
+	Key                         Listable[string] `json:"key,omitempty"`
+	KeyPath                     string           `json:"key_path,omitempty"`
 }
 
 type OutboundECHOptions struct {

+ 6 - 10
outbound/hysteria.go

@@ -11,6 +11,7 @@ import (
 	"github.com/sagernet/quic-go/congestion"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/common/qtls"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -33,7 +34,7 @@ type Hysteria struct {
 	ctx          context.Context
 	dialer       N.Dialer
 	serverAddr   M.Socksaddr
-	tlsConfig    *tls.STDConfig
+	tlsConfig    tls.Config
 	quicConfig   *quic.Config
 	authKey      []byte
 	xplusKey     []byte
@@ -52,17 +53,12 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
 	}
-	abstractTLSConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
+	tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
 	if err != nil {
 		return nil, err
 	}
-	tlsConfig, err := abstractTLSConfig.Config()
-	if err != nil {
-		return nil, err
-	}
-	tlsConfig.MinVersion = tls.VersionTLS13
-	if len(tlsConfig.NextProtos) == 0 {
-		tlsConfig.NextProtos = []string{hysteria.DefaultALPN}
+	if len(tlsConfig.NextProtos()) == 0 {
+		tlsConfig.SetNextProtos([]string{hysteria.DefaultALPN})
 	}
 	quicConfig := &quic.Config{
 		InitialStreamReceiveWindow:     options.ReceiveWindowConn,
@@ -182,7 +178,7 @@ func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
 		packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
 	}
 	packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
-	quicConn, err := quic.Dial(h.ctx, packetConn, udpConn.RemoteAddr(), h.tlsConfig, h.quicConfig)
+	quicConn, err := qtls.Dial(h.ctx, packetConn, udpConn.RemoteAddr(), h.tlsConfig, h.quicConfig)
 	if err != nil {
 		packetConn.Close()
 		return nil, err

+ 1 - 5
outbound/tuic.go

@@ -41,11 +41,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	if options.TLS == nil || !options.TLS.Enabled {
 		return nil, C.ErrTLSRequired
 	}
-	abstractTLSConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
-	if err != nil {
-		return nil, err
-	}
-	tlsConfig, err := abstractTLSConfig.Config()
+	tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
 	if err != nil {
 		return nil, err
 	}

+ 80 - 1
test/ech_test.go

@@ -8,11 +8,13 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
+
+	"github.com/gofrs/uuid/v5"
 )
 
 func TestECH(t *testing.T) {
 	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
-	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("example.org", false))
+	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
 	startInstance(t, option.Options{
 		Inbounds: []option.Inbound{
 			{
@@ -89,3 +91,80 @@ func TestECH(t *testing.T) {
 	})
 	testSuit(t, clientPort, testPort)
 }
+
+func TestECHQUIC(t *testing.T) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeTUIC,
+				TUICOptions: option.TUICInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.NewListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.TUICUser{{
+						UUID: uuid.Nil.String(),
+					}},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+						ECH: &option.InboundECHOptions{
+							Enabled: true,
+							Key:     []string{echKey},
+						},
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTUIC,
+				Tag:  "tuic-out",
+				TUICOptions: option.TUICOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: uuid.Nil.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						ECH: &option.OutboundECHOptions{
+							Enabled: true,
+							Config:  []string{echConfig},
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "tuic-out",
+					},
+				},
+			},
+		},
+	})
+	testSuitLargeUDP(t, clientPort, testPort)
+}

+ 11 - 8
transport/tuic/client.go

@@ -1,8 +1,9 @@
+//go:build with_quic
+
 package tuic
 
 import (
 	"context"
-	"crypto/tls"
 	"io"
 	"net"
 	"runtime"
@@ -10,6 +11,8 @@ import (
 	"time"
 
 	"github.com/sagernet/quic-go"
+	"github.com/sagernet/sing-box/common/qtls"
+	"github.com/sagernet/sing-box/common/tls"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/baderror"
 	"github.com/sagernet/sing/common/buf"
@@ -25,7 +28,7 @@ type ClientOptions struct {
 	Context           context.Context
 	Dialer            N.Dialer
 	ServerAddress     M.Socksaddr
-	TLSConfig         *tls.Config
+	TLSConfig         tls.Config
 	UUID              uuid.UUID
 	Password          string
 	CongestionControl string
@@ -38,7 +41,7 @@ type Client struct {
 	ctx               context.Context
 	dialer            N.Dialer
 	serverAddr        M.Socksaddr
-	tlsConfig         *tls.Config
+	tlsConfig         tls.Config
 	quicConfig        *quic.Config
 	uuid              uuid.UUID
 	password          string
@@ -108,9 +111,9 @@ func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
 	}
 	var quicConn quic.Connection
 	if c.zeroRTTHandshake {
-		quicConn, err = quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
+		quicConn, err = qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
 	} else {
-		quicConn, err = quic.Dial(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
+		quicConn, err = qtls.Dial(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
 	}
 	if err != nil {
 		udpConn.Close()
@@ -141,13 +144,13 @@ func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
 func (c *Client) clientHandshake(conn quic.Connection) error {
 	authStream, err := conn.OpenUniStream()
 	if err != nil {
-		return err
+		return E.Cause(err, "open handshake stream")
 	}
 	defer authStream.Close()
-	handshakeState := conn.ConnectionState().TLS
+	handshakeState := conn.ConnectionState()
 	tuicAuthToken, err := handshakeState.ExportKeyingMaterial(string(c.uuid[:]), []byte(c.password), 32)
 	if err != nil {
-		return err
+		return E.Cause(err, "export keying material")
 	}
 	authRequest := buf.NewSize(AuthenticateLen)
 	authRequest.WriteByte(Version)

+ 2 - 0
transport/tuic/client_packet.go

@@ -1,3 +1,5 @@
+//go:build with_quic
+
 package tuic
 
 import (

+ 9 - 6
transport/tuic/server.go

@@ -1,9 +1,10 @@
+//go:build with_quic
+
 package tuic
 
 import (
 	"bytes"
 	"context"
-	"crypto/tls"
 	"encoding/binary"
 	"io"
 	"net"
@@ -13,6 +14,8 @@ import (
 	"time"
 
 	"github.com/sagernet/quic-go"
+	"github.com/sagernet/sing-box/common/qtls"
+	"github.com/sagernet/sing-box/common/tls"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/auth"
 	"github.com/sagernet/sing/common/baderror"
@@ -29,7 +32,7 @@ import (
 type ServerOptions struct {
 	Context           context.Context
 	Logger            logger.Logger
-	TLSConfig         *tls.Config
+	TLSConfig         tls.ServerConfig
 	Users             []User
 	CongestionControl string
 	AuthTimeout       time.Duration
@@ -52,7 +55,7 @@ type ServerHandler interface {
 type Server struct {
 	ctx               context.Context
 	logger            logger.Logger
-	tlsConfig         *tls.Config
+	tlsConfig         tls.ServerConfig
 	heartbeat         time.Duration
 	quicConfig        *quic.Config
 	userMap           map[uuid.UUID]User
@@ -107,7 +110,7 @@ func NewServer(options ServerOptions) (*Server, error) {
 
 func (s *Server) Start(conn net.PacketConn) error {
 	if !s.quicConfig.Allow0RTT {
-		listener, err := quic.Listen(conn, s.tlsConfig, s.quicConfig)
+		listener, err := qtls.Listen(conn, s.tlsConfig, s.quicConfig)
 		if err != nil {
 			return err
 		}
@@ -127,7 +130,7 @@ func (s *Server) Start(conn net.PacketConn) error {
 			}
 		}()
 	} else {
-		listener, err := quic.ListenEarly(conn, s.tlsConfig, s.quicConfig)
+		listener, err := qtls.ListenEarly(conn, s.tlsConfig, s.quicConfig)
 		if err != nil {
 			return err
 		}
@@ -247,7 +250,7 @@ func (s *serverSession) handleUniStream(stream quic.ReceiveStream) error {
 		if !loaded {
 			return E.New("authentication: unknown user ", userUUID)
 		}
-		handshakeState := s.quicConn.ConnectionState().TLS
+		handshakeState := s.quicConn.ConnectionState()
 		tuicToken, err := handshakeState.ExportKeyingMaterial(string(user.UUID[:]), []byte(user.Password), 32)
 		if err != nil {
 			return E.Cause(err, "authentication: export keying material")

+ 2 - 0
transport/tuic/server_packet.go

@@ -1,3 +1,5 @@
+//go:build with_quic
+
 package tuic
 
 import (

+ 8 - 9
transport/v2rayquic/client.go

@@ -1,3 +1,5 @@
+//go:build with_quic
+
 package v2rayquic
 
 import (
@@ -7,6 +9,7 @@ import (
 
 	"github.com/sagernet/quic-go"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/qtls"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
@@ -23,7 +26,7 @@ type Client struct {
 	ctx        context.Context
 	dialer     N.Dialer
 	serverAddr M.Socksaddr
-	tlsConfig  *tls.STDConfig
+	tlsConfig  tls.Config
 	quicConfig *quic.Config
 	connAccess sync.Mutex
 	conn       quic.Connection
@@ -34,18 +37,14 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
 	quicConfig := &quic.Config{
 		DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows,
 	}
-	stdConfig, err := tlsConfig.Config()
-	if err != nil {
-		return nil, err
-	}
-	if len(stdConfig.NextProtos) == 0 {
-		stdConfig.NextProtos = []string{"h2", "http/1.1"}
+	if len(tlsConfig.NextProtos()) == 0 {
+		tlsConfig.SetNextProtos([]string{"h2", "http/1.1"})
 	}
 	return &Client{
 		ctx:        ctx,
 		dialer:     dialer,
 		serverAddr: serverAddr,
-		tlsConfig:  stdConfig,
+		tlsConfig:  tlsConfig,
 		quicConfig: quicConfig,
 	}, nil
 }
@@ -75,7 +74,7 @@ func (c *Client) offerNew() (quic.Connection, error) {
 	}
 	var packetConn net.PacketConn
 	packetConn = bufio.NewUnbindPacketConn(udpConn)
-	quicConn, err := quic.Dial(c.ctx, packetConn, udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
+	quicConn, err := qtls.Dial(c.ctx, packetConn, udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
 	if err != nil {
 		packetConn.Close()
 		return nil, err

+ 2 - 0
transport/v2rayquic/init.go

@@ -1,3 +1,5 @@
+//go:build with_quic
+
 package v2rayquic
 
 import "github.com/sagernet/sing-box/transport/v2ray"

+ 10 - 11
transport/v2rayquic/server.go

@@ -1,3 +1,5 @@
+//go:build with_quic
+
 package v2rayquic
 
 import (
@@ -7,6 +9,7 @@ import (
 
 	"github.com/sagernet/quic-go"
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/qtls"
 	"github.com/sagernet/sing-box/common/tls"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
@@ -20,27 +23,23 @@ var _ adapter.V2RayServerTransport = (*Server)(nil)
 
 type Server struct {
 	ctx          context.Context
-	tlsConfig    *tls.STDConfig
+	tlsConfig    tls.ServerConfig
 	quicConfig   *quic.Config
 	handler      adapter.V2RayServerTransportHandler
 	udpListener  net.PacketConn
-	quicListener *quic.Listener
+	quicListener qtls.QUICListener
 }
 
 func NewServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) {
 	quicConfig := &quic.Config{
 		DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows,
 	}
-	stdConfig, err := tlsConfig.Config()
-	if err != nil {
-		return nil, err
-	}
-	if len(stdConfig.NextProtos) == 0 {
-		stdConfig.NextProtos = []string{"h2", "http/1.1"}
+	if len(tlsConfig.NextProtos()) == 0 {
+		tlsConfig.SetNextProtos([]string{"h2", "http/1.1"})
 	}
 	server := &Server{
 		ctx:        ctx,
-		tlsConfig:  stdConfig,
+		tlsConfig:  tlsConfig,
 		quicConfig: quicConfig,
 		handler:    handler,
 	}
@@ -56,7 +55,7 @@ func (s *Server) Serve(listener net.Listener) error {
 }
 
 func (s *Server) ServePacket(listener net.PacketConn) error {
-	quicListener, err := quic.Listen(listener, s.tlsConfig, s.quicConfig)
+	quicListener, err := qtls.Listen(listener, s.tlsConfig, s.quicConfig)
 	if err != nil {
 		return err
 	}
@@ -92,5 +91,5 @@ func (s *Server) streamAcceptLoop(conn quic.Connection) error {
 }
 
 func (s *Server) Close() error {
-	return common.Close(s.udpListener, common.PtrOrNil(s.quicListener))
+	return common.Close(s.udpListener, s.quicListener)
 }