diff --git a/common/qtls/wrapper.go b/common/qtls/wrapper.go new file mode 100644 index 00000000..67ca47a6 --- /dev/null +++ b/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 +} diff --git a/common/tls/ech_client.go b/common/tls/ech_client.go index 5b4d72da..60560387 100644 --- a/common/tls/ech_client.go +++ b/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) { diff --git a/common/tls/ech_quic.go b/common/tls/ech_quic.go new file mode 100644 index 00000000..4ed4cec1 --- /dev/null +++ b/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) +} diff --git a/common/tls/ech_server.go b/common/tls/ech_server.go index 412599ed..43ddd820 100644 --- a/common/tls/ech_server.go +++ b/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") } diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 7f804933..a5f08240 100644 --- a/docs/configuration/shared/tls.md +++ b/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 "" diff --git a/inbound/hysteria.go b/inbound/hysteria.go index 6e94e7d0..93a32e9d 100644 --- a/inbound/hysteria.go +++ b/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, ) } diff --git a/inbound/naive_quic.go b/inbound/naive_quic.go index f518796f..7a17b01f 100644 --- a/inbound/naive_quic.go +++ b/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) diff --git a/inbound/tuic.go b/inbound/tuic.go index 65b6e3b3..a8547bf6 100644 --- a/inbound/tuic.go +++ b/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), diff --git a/option/tls.go b/option/tls.go index 1f9f5746..63944980 100644 --- a/option/tls.go +++ b/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 { diff --git a/outbound/hysteria.go b/outbound/hysteria.go index 456ace50..c236f759 100644 --- a/outbound/hysteria.go +++ b/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 diff --git a/outbound/tuic.go b/outbound/tuic.go index e20cd411..e8c3f700 100644 --- a/outbound/tuic.go +++ b/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 } diff --git a/test/ech_test.go b/test/ech_test.go index b05351b4..a792d8c1 100644 --- a/test/ech_test.go +++ b/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) +} diff --git a/transport/tuic/client.go b/transport/tuic/client.go index 997379c1..88967723 100644 --- a/transport/tuic/client.go +++ b/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) diff --git a/transport/tuic/client_packet.go b/transport/tuic/client_packet.go index 48da97a5..eb660848 100644 --- a/transport/tuic/client_packet.go +++ b/transport/tuic/client_packet.go @@ -1,3 +1,5 @@ +//go:build with_quic + package tuic import ( diff --git a/transport/tuic/server.go b/transport/tuic/server.go index 01a2644a..d4f6de8f 100644 --- a/transport/tuic/server.go +++ b/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") diff --git a/transport/tuic/server_packet.go b/transport/tuic/server_packet.go index d05c7bf1..5a26cf50 100644 --- a/transport/tuic/server_packet.go +++ b/transport/tuic/server_packet.go @@ -1,3 +1,5 @@ +//go:build with_quic + package tuic import ( diff --git a/transport/v2rayquic/client.go b/transport/v2rayquic/client.go index ff8b538c..b8037d95 100644 --- a/transport/v2rayquic/client.go +++ b/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 diff --git a/transport/v2rayquic/init.go b/transport/v2rayquic/init.go index 45933ebe..83c7fc3d 100644 --- a/transport/v2rayquic/init.go +++ b/transport/v2rayquic/init.go @@ -1,3 +1,5 @@ +//go:build with_quic + package v2rayquic import "github.com/sagernet/sing-box/transport/v2ray" diff --git a/transport/v2rayquic/server.go b/transport/v2rayquic/server.go index 870312e9..c366006e 100644 --- a/transport/v2rayquic/server.go +++ b/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) }