Przeglądaj źródła

Add shadow-tls support

世界 2 lat temu
rodzic
commit
d6c2a9aab7

+ 46 - 0
common/singbridge/destination.go

@@ -0,0 +1,46 @@
+package singbridge
+
+import (
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/xtls/xray-core/common/net"
+)
+
+func ToNetwork(network string) net.Network {
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		return net.Network_TCP
+	case N.NetworkUDP:
+		return net.Network_UDP
+	default:
+		return net.Network_Unknown
+	}
+}
+
+func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination {
+	if socksaddr.IsFqdn() {
+		return net.Destination{
+			Network: network,
+			Address: net.DomainAddress(socksaddr.Fqdn),
+			Port:    net.Port(socksaddr.Port),
+		}
+	} else {
+		return net.Destination{
+			Network: network,
+			Address: net.IPAddress(socksaddr.Addr.AsSlice()),
+			Port:    net.Port(socksaddr.Port),
+		}
+	}
+}
+
+func ToSocksaddr(destination net.Destination) M.Socksaddr {
+	var addr M.Socksaddr
+	switch destination.Address.Family() {
+	case net.AddressFamilyDomain:
+		addr.Fqdn = destination.Address.Domain()
+	default:
+		addr.Addr = M.AddrFromIP(destination.Address.IP())
+	}
+	addr.Port = uint16(destination.Port)
+	return addr
+}

+ 29 - 0
common/singbridge/dialer.go

@@ -0,0 +1,29 @@
+package singbridge
+
+import (
+	"context"
+	"os"
+
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/transport/internet"
+)
+
+var _ N.Dialer = (*XrayDialer)(nil)
+
+type XrayDialer struct {
+	internet.Dialer
+}
+
+func NewDialer(dialer internet.Dialer) *XrayDialer {
+	return &XrayDialer{dialer}
+}
+
+func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return d.Dialer.Dial(ctx, ToDestination(destination, ToNetwork(network)))
+}
+
+func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return nil, os.ErrInvalid
+}

+ 41 - 0
common/singbridge/dialer_tls.go

@@ -0,0 +1,41 @@
+package singbridge
+
+import (
+	"context"
+	gotls "crypto/tls"
+	"os"
+
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/transport/internet"
+	"github.com/xtls/xray-core/transport/internet/tls"
+)
+
+type XrayTLSDialer struct {
+	dialer     internet.Dialer
+	clientFunc tls.CustomClientFunc
+}
+
+func NewTLSDialer(dialer internet.Dialer, clientFunc tls.CustomClientFunc) *XrayTLSDialer {
+	return &XrayTLSDialer{dialer, clientFunc}
+}
+
+func (d *XrayTLSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	var tlsConfig *gotls.Config
+	conn, err := d.dialer.Dial(tls.ContextWithCustomClient(ctx, func(conn net.Conn, config *gotls.Config) net.Conn {
+		tlsConfig = config
+		return conn
+	}), ToDestination(destination, ToNetwork(network)))
+	if err != nil {
+		return nil, err
+	}
+	if tlsConfig == nil {
+		return nil, E.New("missing TLS config")
+	}
+	return d.clientFunc(conn, tlsConfig), nil
+}
+
+func (d *XrayTLSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return nil, os.ErrInvalid
+}

+ 10 - 0
common/singbridge/error.go

@@ -0,0 +1,10 @@
+package singbridge
+
+import E "github.com/sagernet/sing/common/exceptions"
+
+func ReturnError(err error) error {
+	if E.IsClosed(err) {
+		return nil
+	}
+	return err
+}

+ 70 - 0
common/singbridge/logger.go

@@ -0,0 +1,70 @@
+package singbridge
+
+import (
+	"context"
+
+	"github.com/sagernet/sing/common/logger"
+	"github.com/xtls/xray-core/common/errors"
+)
+
+var _ logger.ContextLogger = (*XrayLogger)(nil)
+
+type XrayLogger struct {
+	newError func(values ...any) *errors.Error
+}
+
+func NewLogger(newErrorFunc func(values ...any) *errors.Error) *XrayLogger {
+	return &XrayLogger{
+		newErrorFunc,
+	}
+}
+
+func (l *XrayLogger) Trace(args ...any) {
+}
+
+func (l *XrayLogger) Debug(args ...any) {
+	l.newError(args...).AtDebug().WriteToLog()
+}
+
+func (l *XrayLogger) Info(args ...any) {
+	l.newError(args...).AtInfo().WriteToLog()
+}
+
+func (l *XrayLogger) Warn(args ...any) {
+	l.newError(args...).AtWarning().WriteToLog()
+}
+
+func (l *XrayLogger) Error(args ...any) {
+	l.newError(args...).AtError().WriteToLog()
+}
+
+func (l *XrayLogger) Fatal(args ...any) {
+}
+
+func (l *XrayLogger) Panic(args ...any) {
+}
+
+func (l *XrayLogger) TraceContext(ctx context.Context, args ...any) {
+}
+
+func (l *XrayLogger) DebugContext(ctx context.Context, args ...any) {
+	l.newError(args...).AtDebug().WriteToLog()
+}
+
+func (l *XrayLogger) InfoContext(ctx context.Context, args ...any) {
+	l.newError(args...).AtInfo().WriteToLog()
+}
+
+func (l *XrayLogger) WarnContext(ctx context.Context, args ...any) {
+	l.newError(args...).AtWarning().WriteToLog()
+}
+
+func (l *XrayLogger) ErrorContext(ctx context.Context, args ...any) {
+	l.newError(args...).AtError().WriteToLog()
+}
+
+func (l *XrayLogger) FatalContext(ctx context.Context, args ...any) {
+}
+
+func (l *XrayLogger) PanicContext(ctx context.Context, args ...any) {
+}

+ 61 - 0
common/singbridge/pipe.go

@@ -0,0 +1,61 @@
+package singbridge
+
+import (
+	"context"
+	"io"
+	"net"
+
+	"github.com/sagernet/sing/common/bufio"
+	"github.com/xtls/xray-core/common/buf"
+	"github.com/xtls/xray-core/transport"
+)
+
+func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, serverConn net.Conn) error {
+	conn := &PipeConnWrapper{
+		W:    link.Writer,
+		Conn: inboundConn,
+	}
+	if ir, ok := link.Reader.(io.Reader); ok {
+		conn.R = ir
+	} else {
+		conn.R = &buf.BufferedReader{Reader: link.Reader}
+	}
+	return ReturnError(bufio.CopyConn(ctx, conn, serverConn))
+}
+
+type PipeConnWrapper struct {
+	R io.Reader
+	W buf.Writer
+	net.Conn
+}
+
+func (w *PipeConnWrapper) Close() error {
+	return nil
+}
+
+func (w *PipeConnWrapper) Read(b []byte) (n int, err error) {
+	return w.R.Read(b)
+}
+
+func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
+	n = len(p)
+	var mb buf.MultiBuffer
+	pLen := len(p)
+	for pLen > 0 {
+		buffer := buf.New()
+		if pLen > buf.Size {
+			_, err = buffer.Write(p[:buf.Size])
+			p = p[buf.Size:]
+		} else {
+			buffer.Write(p)
+		}
+		pLen -= int(buffer.Len())
+		mb = append(mb, buffer)
+	}
+	err = w.W.WriteMultiBuffer(mb)
+	if err != nil {
+		n = 0
+		buf.ReleaseMulti(mb)
+	}
+	return
+}

+ 1 - 0
go.mod

@@ -15,6 +15,7 @@ require (
 	github.com/refraction-networking/utls v1.2.2
 	github.com/sagernet/sing v0.1.6
 	github.com/sagernet/sing-shadowsocks v0.1.1-0.20230202035033-e3123545f2f7
+	github.com/sagernet/sing-shadowtls v0.0.0-20230221100347-75f55ea45b99
 	github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c
 	github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb
 	github.com/stretchr/testify v1.8.1

+ 8 - 0
go.sum

@@ -147,6 +147,14 @@ github.com/sagernet/sing v0.1.6 h1:Qy63OUfKpcqKjfd5rPmUlj0RGjHZSK/PJn0duyCCsRg=
 github.com/sagernet/sing v0.1.6/go.mod h1:JLSXsPTGRJFo/3X7EcAOCUgJH2/gAoxSJgBsnCZRp/w=
 github.com/sagernet/sing-shadowsocks v0.1.1-0.20230202035033-e3123545f2f7 h1:Plup6oEiyLzY3HDqQ+QsUBzgBGdVmcsgf3t8h940z9U=
 github.com/sagernet/sing-shadowsocks v0.1.1-0.20230202035033-e3123545f2f7/go.mod h1:O5LtOs8Ivw686FqLpO0Zu+A0ROVE15VeqEK3yDRRAms=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221081357-574313aaae1d h1:yETevmRbJ6Mf9xgavSmgxo9UCdKNyplU4ubgFXqd4XU=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221081357-574313aaae1d/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221093358-af0356df4755 h1:5/GdWkRlHv00+4JrIy0ilWAw2p/EWtuH0CClC2/gko4=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221093358-af0356df4755/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221100347-75f55ea45b99 h1:LhGnlaH8bV0MCNp/LW4PDPagtkJDSAX0ftN9bJ6HMxY=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221100347-75f55ea45b99/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221110738-214729669cdc h1:T1XsW+0eNGlyy7NXY8AG2pE3d+RsdphRCH5ux0jjWf4=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221110738-214729669cdc/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI=
 github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=

+ 75 - 0
infra/conf/shadowtls.go

@@ -0,0 +1,75 @@
+package conf
+
+import (
+	"github.com/golang/protobuf/proto"
+	"github.com/sagernet/sing/common"
+	"github.com/xtls/xray-core/proxy/shadowtls"
+)
+
+type ShadowTLSServerConfig struct {
+	Version                uint16                               `json:"version"`
+	Password               string                               `json:"password,omitempty"`
+	Users                  []ShadowTLSUser                      `json:"users,omitempty"`
+	Handshake              *ShadowTLSHandshakeConfig            `json:"handshake"`
+	HandshakeForServerName map[string]*ShadowTLSHandshakeConfig `json:"handshakeForServerName,omitempty"`
+	StrictMode             bool                                 `json:"strictMode,omitempty"`
+	Detour                 string                               `json:"detour"`
+}
+
+type ShadowTLSUser struct {
+	Email    string `json:"email,omitempty"`
+	Password string `json:"password,omitempty"`
+}
+
+func (c ShadowTLSUser) Build() *shadowtls.User {
+	return &shadowtls.User{
+		Email:    c.Email,
+		Password: c.Password,
+	}
+}
+
+type ShadowTLSHandshakeConfig struct {
+	Address *Address `json:"address"`
+	Port    uint16   `json:"port"`
+}
+
+func (c ShadowTLSHandshakeConfig) Build() *shadowtls.HandshakeConfig {
+	return &shadowtls.HandshakeConfig{
+		Address: c.Address.Build(),
+		Port:    uint32(c.Port),
+	}
+}
+
+func (c *ShadowTLSServerConfig) Build() (proto.Message, error) {
+	var handshakeForServerName map[string]*shadowtls.HandshakeConfig
+	if c.HandshakeForServerName != nil {
+		for serverName, serverConfig := range c.HandshakeForServerName {
+			handshakeForServerName[serverName] = serverConfig.Build()
+		}
+	}
+	return &shadowtls.ServerConfig{
+		Version:                uint32(c.Version),
+		Password:               c.Password,
+		Users:                  common.Map(c.Users, ShadowTLSUser.Build),
+		Handshake:              c.Handshake.Build(),
+		HandshakeForServerName: handshakeForServerName,
+		StrictMode:             c.StrictMode,
+		Detour:                 c.Detour,
+	}, nil
+}
+
+type ShadowTLSClientConfig struct {
+	Address  *Address `json:"address"`
+	Port     uint16   `json:"port"`
+	Version  uint16   `json:"version"`
+	Password string   `json:"password,omitempty"`
+}
+
+func (c *ShadowTLSClientConfig) Build() (proto.Message, error) {
+	return &shadowtls.ClientConfig{
+		Address:  c.Address.Build(),
+		Port:     uint32(c.Port),
+		Version:  uint32(c.Version),
+		Password: c.Password,
+	}, nil
+}

+ 2 - 0
infra/conf/xray.go

@@ -26,6 +26,7 @@ var (
 		"vmess":         func() interface{} { return new(VMessInboundConfig) },
 		"trojan":        func() interface{} { return new(TrojanServerConfig) },
 		"mtproto":       func() interface{} { return new(MTProtoServerConfig) },
+		"shadow-tls":    func() interface{} { return new(ShadowTLSServerConfig) },
 	}, "protocol", "settings")
 
 	outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
@@ -41,6 +42,7 @@ var (
 		"mtproto":     func() interface{} { return new(MTProtoClientConfig) },
 		"dns":         func() interface{} { return new(DNSOutboundConfig) },
 		"wireguard":   func() interface{} { return new(WireGuardConfig) },
+		"shadow-tls":  func() interface{} { return new(ShadowTLSClientConfig) },
 	}, "protocol", "settings")
 
 	ctllog = log.New(os.Stderr, "xctl> ", 0)

+ 1 - 0
main/distro/all/all.go

@@ -42,6 +42,7 @@ import (
 	_ "github.com/xtls/xray-core/proxy/loopback"
 	_ "github.com/xtls/xray-core/proxy/mtproto"
 	_ "github.com/xtls/xray-core/proxy/shadowsocks"
+	_ "github.com/xtls/xray-core/proxy/shadowtls"
 	_ "github.com/xtls/xray-core/proxy/socks"
 	_ "github.com/xtls/xray-core/proxy/trojan"
 	_ "github.com/xtls/xray-core/proxy/vless/inbound"

+ 6 - 10
proxy/shadowsocks_2022/inbound.go

@@ -17,6 +17,7 @@ import (
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
 	"github.com/xtls/xray-core/features/routing"
 	"github.com/xtls/xray-core/transport/internet/stat"
 )
@@ -73,7 +74,7 @@ func (i *Inbound) Process(ctx context.Context, network net.Network, connection s
 	ctx = session.ContextWithDispatcher(ctx, dispatcher)
 
 	if network == net.Network_TCP {
-		return returnError(i.service.NewConnection(ctx, connection, metadata))
+		return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
 	} else {
 		reader := buf.NewReader(connection)
 		pc := &natPacketConn{connection}
@@ -81,7 +82,7 @@ func (i *Inbound) Process(ctx context.Context, network net.Network, connection s
 			mb, err := reader.ReadMultiBuffer()
 			if err != nil {
 				buf.ReleaseMulti(mb)
-				return returnError(err)
+				return singbridge.ReturnError(err)
 			}
 			for _, buffer := range mb {
 				packet := B.As(buffer.Bytes()).ToOwned()
@@ -111,16 +112,11 @@ func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.M
 	})
 	newError("tunnelling request to tcp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	link, err := dispatcher.Dispatch(ctx, toDestination(metadata.Destination, net.Network_TCP))
+	link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP))
 	if err != nil {
 		return err
 	}
-	outConn := &pipeConnWrapper{
-		&buf.BufferedReader{Reader: link.Reader},
-		link.Writer,
-		conn,
-	}
-	return bufio.CopyConn(ctx, conn, outConn)
+	return singbridge.CopyConn(ctx, nil, link, conn)
 }
 
 func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
@@ -137,7 +133,7 @@ func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, me
 	})
 	newError("tunnelling request to udp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	destination := toDestination(metadata.Destination, net.Network_UDP)
+	destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
 	link, err := dispatcher.Dispatch(ctx, destination)
 	if err != nil {
 		return err

+ 6 - 10
proxy/shadowsocks_2022/inbound_multi.go

@@ -21,6 +21,7 @@ import (
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
 	"github.com/xtls/xray-core/common/uuid"
 	"github.com/xtls/xray-core/features/routing"
 	"github.com/xtls/xray-core/transport/internet/stat"
@@ -162,7 +163,7 @@ func (i *MultiUserInbound) Process(ctx context.Context, network net.Network, con
 	ctx = session.ContextWithDispatcher(ctx, dispatcher)
 
 	if network == net.Network_TCP {
-		return returnError(i.service.NewConnection(ctx, connection, metadata))
+		return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
 	} else {
 		reader := buf.NewReader(connection)
 		pc := &natPacketConn{connection}
@@ -170,7 +171,7 @@ func (i *MultiUserInbound) Process(ctx context.Context, network net.Network, con
 			mb, err := reader.ReadMultiBuffer()
 			if err != nil {
 				buf.ReleaseMulti(mb)
-				return returnError(err)
+				return singbridge.ReturnError(err)
 			}
 			for _, buffer := range mb {
 				packet := B.As(buffer.Bytes()).ToOwned()
@@ -202,16 +203,11 @@ func (i *MultiUserInbound) NewConnection(ctx context.Context, conn net.Conn, met
 	})
 	newError("tunnelling request to tcp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	link, err := dispatcher.Dispatch(ctx, toDestination(metadata.Destination, net.Network_TCP))
+	link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP))
 	if err != nil {
 		return err
 	}
-	outConn := &pipeConnWrapper{
-		&buf.BufferedReader{Reader: link.Reader},
-		link.Writer,
-		conn,
-	}
-	return bufio.CopyConn(ctx, conn, outConn)
+	return singbridge.CopyConn(ctx, conn, link, conn)
 }
 
 func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
@@ -230,7 +226,7 @@ func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.Packe
 	})
 	newError("tunnelling request to udp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	destination := toDestination(metadata.Destination, net.Network_UDP)
+	destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
 	link, err := dispatcher.Dispatch(ctx, destination)
 	if err != nil {
 		return err

+ 7 - 11
proxy/shadowsocks_2022/inbound_relay.go

@@ -19,6 +19,7 @@ import (
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
 	"github.com/xtls/xray-core/common/uuid"
 	"github.com/xtls/xray-core/features/routing"
 	"github.com/xtls/xray-core/transport/internet/stat"
@@ -66,7 +67,7 @@ func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbou
 		C.MapIndexed(config.Destinations, func(index int, it *RelayDestination) int { return index }),
 		C.Map(config.Destinations, func(it *RelayDestination) string { return it.Key }),
 		C.Map(config.Destinations, func(it *RelayDestination) M.Socksaddr {
-			return toSocksaddr(net.Destination{
+			return singbridge.ToSocksaddr(net.Destination{
 				Address: it.Address.AsAddress(),
 				Port:    net.Port(it.Port),
 			})
@@ -94,7 +95,7 @@ func (i *RelayInbound) Process(ctx context.Context, network net.Network, connect
 	ctx = session.ContextWithDispatcher(ctx, dispatcher)
 
 	if network == net.Network_TCP {
-		return returnError(i.service.NewConnection(ctx, connection, metadata))
+		return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
 	} else {
 		reader := buf.NewReader(connection)
 		pc := &natPacketConn{connection}
@@ -102,7 +103,7 @@ func (i *RelayInbound) Process(ctx context.Context, network net.Network, connect
 			mb, err := reader.ReadMultiBuffer()
 			if err != nil {
 				buf.ReleaseMulti(mb)
-				return returnError(err)
+				return singbridge.ReturnError(err)
 			}
 			for _, buffer := range mb {
 				packet := B.As(buffer.Bytes()).ToOwned()
@@ -134,16 +135,11 @@ func (i *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadat
 	})
 	newError("tunnelling request to tcp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	link, err := dispatcher.Dispatch(ctx, toDestination(metadata.Destination, net.Network_TCP))
+	link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP))
 	if err != nil {
 		return err
 	}
-	outConn := &pipeConnWrapper{
-		&buf.BufferedReader{Reader: link.Reader},
-		link.Writer,
-		conn,
-	}
-	return bufio.CopyConn(ctx, conn, outConn)
+	return singbridge.CopyConn(ctx, nil, link, conn)
 }
 
 func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
@@ -162,7 +158,7 @@ func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketCon
 	})
 	newError("tunnelling request to udp:", metadata.Destination).WriteToLog(session.ExportIDToError(ctx))
 	dispatcher := session.DispatcherFromContext(ctx)
-	destination := toDestination(metadata.Destination, net.Network_UDP)
+	destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
 	link, err := dispatcher.Dispatch(ctx, destination)
 	if err != nil {
 		return err

+ 5 - 15
proxy/shadowsocks_2022/outbound.go

@@ -2,7 +2,6 @@ package shadowsocks_2022
 
 import (
 	"context"
-	"io"
 	"runtime"
 	"time"
 
@@ -18,6 +17,7 @@ import (
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
 	"github.com/xtls/xray-core/transport"
 	"github.com/xtls/xray-core/transport/internet"
 )
@@ -88,7 +88,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
 	}
 
 	if network == net.Network_TCP {
-		serverConn := o.method.DialEarlyConn(connection, toSocksaddr(destination))
+		serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
 		var handshake bool
 		if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader {
 			mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100)
@@ -123,17 +123,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
 				return newError("client handshake").Base(err)
 			}
 		}
-		conn := &pipeConnWrapper{
-			W:    link.Writer,
-			Conn: inboundConn,
-		}
-		if ir, ok := link.Reader.(io.Reader); ok {
-			conn.R = ir
-		} else {
-			conn.R = &buf.BufferedReader{Reader: link.Reader}
-		}
-
-		return returnError(bufio.CopyConn(ctx, conn, serverConn))
+		return singbridge.CopyConn(ctx, inboundConn, link, serverConn)
 	} else {
 		var packetConn N.PacketConn
 		if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn {
@@ -151,10 +141,10 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
 
 		if o.uot {
 			serverConn := o.method.DialEarlyConn(connection, M.Socksaddr{Fqdn: uot.UOTMagicAddress})
-			return returnError(bufio.CopyPacketConn(ctx, packetConn, uot.NewClientConn(serverConn)))
+			return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, uot.NewClientConn(serverConn)))
 		} else {
 			serverConn := o.method.DialPacketConn(connection)
-			return returnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
+			return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
 		}
 	}
 }

+ 4 - 78
proxy/shadowsocks_2022/shadowsocks_2022.go

@@ -1,82 +1,15 @@
 package shadowsocks_2022
 
 import (
-	"io"
-
 	B "github.com/sagernet/sing/common/buf"
-	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/singbridge"
 )
 
 //go:generate go run github.com/xtls/xray-core/common/errors/errorgen
 
-func toDestination(socksaddr M.Socksaddr, network net.Network) net.Destination {
-	if socksaddr.IsFqdn() {
-		return net.Destination{
-			Network: network,
-			Address: net.DomainAddress(socksaddr.Fqdn),
-			Port:    net.Port(socksaddr.Port),
-		}
-	} else {
-		return net.Destination{
-			Network: network,
-			Address: net.IPAddress(socksaddr.Addr.AsSlice()),
-			Port:    net.Port(socksaddr.Port),
-		}
-	}
-}
-
-func toSocksaddr(destination net.Destination) M.Socksaddr {
-	var addr M.Socksaddr
-	switch destination.Address.Family() {
-	case net.AddressFamilyDomain:
-		addr.Fqdn = destination.Address.Domain()
-	default:
-		addr.Addr = M.AddrFromIP(destination.Address.IP())
-	}
-	addr.Port = uint16(destination.Port)
-	return addr
-}
-
-type pipeConnWrapper struct {
-	R io.Reader
-	W buf.Writer
-	net.Conn
-}
-
-func (w *pipeConnWrapper) Close() error {
-	return nil
-}
-
-func (w *pipeConnWrapper) Read(b []byte) (n int, err error) {
-	return w.R.Read(b)
-}
-
-func (w *pipeConnWrapper) Write(p []byte) (n int, err error) {
-	n = len(p)
-	var mb buf.MultiBuffer
-	pLen := len(p)
-	for pLen > 0 {
-		buffer := buf.New()
-		if pLen > buf.Size {
-			_, err = buffer.Write(p[:buf.Size])
-			p = p[buf.Size:]
-		} else {
-			buffer.Write(p)
-		}
-		pLen -= int(buffer.Len())
-		mb = append(mb, buffer)
-	}
-	err = w.W.WriteMultiBuffer(mb)
-	if err != nil {
-		n = 0
-		buf.ReleaseMulti(mb)
-	}
-	return
-}
-
 type packetConnWrapper struct {
 	buf.Reader
 	buf.Writer
@@ -100,7 +33,7 @@ func (w *packetConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
 				destination = w.Dest
 			}
 			bb.Release()
-			return toSocksaddr(destination), nil
+			return singbridge.ToSocksaddr(destination), nil
 		}
 	}
 	mb, err := w.ReadMultiBuffer()
@@ -120,14 +53,14 @@ func (w *packetConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
 			destination = w.Dest
 		}
 		bb.Release()
-		return toSocksaddr(destination), nil
+		return singbridge.ToSocksaddr(destination), nil
 	}
 }
 
 func (w *packetConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) error {
 	vBuf := buf.New()
 	vBuf.Write(buffer.Bytes())
-	endpoint := toDestination(destination, net.Network_UDP)
+	endpoint := singbridge.ToDestination(destination, net.Network_UDP)
 	vBuf.UDP = &endpoint
 	return w.Writer.WriteMultiBuffer(buf.MultiBuffer{vBuf})
 }
@@ -136,10 +69,3 @@ func (w *packetConnWrapper) Close() error {
 	buf.ReleaseMulti(w.cached)
 	return nil
 }
-
-func returnError(err error) error {
-	if E.IsClosed(err) {
-		return nil
-	}
-	return err
-}

+ 483 - 0
proxy/shadowtls/config.pb.go

@@ -0,0 +1,483 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.28.1
+// 	protoc        v3.21.12
+// source: proxy/shadowtls/config.proto
+
+package shadowtls
+
+import (
+	net "github.com/xtls/xray-core/common/net"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type ServerConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version                uint32                      `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Password               string                      `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+	Users                  []*User                     `protobuf:"bytes,3,rep,name=users,proto3" json:"users,omitempty"`
+	Handshake              *HandshakeConfig            `protobuf:"bytes,4,opt,name=handshake,proto3" json:"handshake,omitempty"`
+	HandshakeForServerName map[string]*HandshakeConfig `protobuf:"bytes,5,rep,name=handshake_for_server_name,json=handshakeForServerName,proto3" json:"handshake_for_server_name,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	StrictMode             bool                        `protobuf:"varint,6,opt,name=strict_mode,json=strictMode,proto3" json:"strict_mode,omitempty"`
+	Detour                 string                      `protobuf:"bytes,7,opt,name=detour,proto3" json:"detour,omitempty"`
+}
+
+func (x *ServerConfig) Reset() {
+	*x = ServerConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_proxy_shadowtls_config_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ServerConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServerConfig) ProtoMessage() {}
+
+func (x *ServerConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_proxy_shadowtls_config_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead.
+func (*ServerConfig) Descriptor() ([]byte, []int) {
+	return file_proxy_shadowtls_config_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ServerConfig) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *ServerConfig) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+func (x *ServerConfig) GetUsers() []*User {
+	if x != nil {
+		return x.Users
+	}
+	return nil
+}
+
+func (x *ServerConfig) GetHandshake() *HandshakeConfig {
+	if x != nil {
+		return x.Handshake
+	}
+	return nil
+}
+
+func (x *ServerConfig) GetHandshakeForServerName() map[string]*HandshakeConfig {
+	if x != nil {
+		return x.HandshakeForServerName
+	}
+	return nil
+}
+
+func (x *ServerConfig) GetStrictMode() bool {
+	if x != nil {
+		return x.StrictMode
+	}
+	return false
+}
+
+func (x *ServerConfig) GetDetour() string {
+	if x != nil {
+		return x.Detour
+	}
+	return ""
+}
+
+type HandshakeConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+	Port    uint32          `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
+}
+
+func (x *HandshakeConfig) Reset() {
+	*x = HandshakeConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_proxy_shadowtls_config_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HandshakeConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HandshakeConfig) ProtoMessage() {}
+
+func (x *HandshakeConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_proxy_shadowtls_config_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HandshakeConfig.ProtoReflect.Descriptor instead.
+func (*HandshakeConfig) Descriptor() ([]byte, []int) {
+	return file_proxy_shadowtls_config_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HandshakeConfig) GetAddress() *net.IPOrDomain {
+	if x != nil {
+		return x.Address
+	}
+	return nil
+}
+
+func (x *HandshakeConfig) GetPort() uint32 {
+	if x != nil {
+		return x.Port
+	}
+	return 0
+}
+
+type User struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Email    string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
+	Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+	Level    int32  `protobuf:"varint,3,opt,name=level,proto3" json:"level,omitempty"`
+}
+
+func (x *User) Reset() {
+	*x = User{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_proxy_shadowtls_config_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *User) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*User) ProtoMessage() {}
+
+func (x *User) ProtoReflect() protoreflect.Message {
+	mi := &file_proxy_shadowtls_config_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use User.ProtoReflect.Descriptor instead.
+func (*User) Descriptor() ([]byte, []int) {
+	return file_proxy_shadowtls_config_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *User) GetEmail() string {
+	if x != nil {
+		return x.Email
+	}
+	return ""
+}
+
+func (x *User) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+func (x *User) GetLevel() int32 {
+	if x != nil {
+		return x.Level
+	}
+	return 0
+}
+
+type ClientConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Address  *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+	Port     uint32          `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
+	Version  uint32          `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"`
+	Password string          `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
+}
+
+func (x *ClientConfig) Reset() {
+	*x = ClientConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_proxy_shadowtls_config_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ClientConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClientConfig) ProtoMessage() {}
+
+func (x *ClientConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_proxy_shadowtls_config_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead.
+func (*ClientConfig) Descriptor() ([]byte, []int) {
+	return file_proxy_shadowtls_config_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ClientConfig) GetAddress() *net.IPOrDomain {
+	if x != nil {
+		return x.Address
+	}
+	return nil
+}
+
+func (x *ClientConfig) GetPort() uint32 {
+	if x != nil {
+		return x.Port
+	}
+	return 0
+}
+
+func (x *ClientConfig) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *ClientConfig) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+var File_proxy_shadowtls_config_proto protoreflect.FileDescriptor
+
+var file_proxy_shadowtls_config_proto_rawDesc = []byte{
+	0x0a, 0x1c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c,
+	0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14,
+	0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f,
+	0x77, 0x74, 0x6c, 0x73, 0x1a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74,
+	0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe1,
+	0x03, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+	0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
+	0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73,
+	0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
+	0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78,
+	0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72,
+	0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x43, 0x0a, 0x09, 0x68, 0x61, 0x6e, 0x64, 0x73,
+	0x68, 0x61, 0x6b, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61,
+	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c,
+	0x73, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+	0x67, 0x52, 0x09, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x79, 0x0a, 0x19,
+	0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x73, 0x65,
+	0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x3e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61,
+	0x64, 0x6f, 0x77, 0x74, 0x6c, 0x73, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e,
+	0x66, 0x69, 0x67, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x46, 0x6f, 0x72,
+	0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
+	0x16, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72,
+	0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x63,
+	0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x74,
+	0x72, 0x69, 0x63, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x6f,
+	0x75, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x6f, 0x75, 0x72,
+	0x1a, 0x70, 0x0a, 0x1b, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x46, 0x6f, 0x72,
+	0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
+	0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68,
+	0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c, 0x73, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b,
+	0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
+	0x38, 0x01, 0x22, 0x5c, 0x0a, 0x0f, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x43,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
+	0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04,
+	0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74,
+	0x22, 0x4e, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69,
+	0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a,
+	0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65,
+	0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
+	0x22, 0x8f, 0x01, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+	0x67, 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
+	0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52,
+	0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x18, 0x0a, 0x07,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
+	0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
+	0x72, 0x64, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70,
+	0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c, 0x73, 0x50, 0x01,
+	0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c,
+	0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78,
+	0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x14, 0x58, 0x72,
+	0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x54,
+	0x4c, 0x53, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_proxy_shadowtls_config_proto_rawDescOnce sync.Once
+	file_proxy_shadowtls_config_proto_rawDescData = file_proxy_shadowtls_config_proto_rawDesc
+)
+
+func file_proxy_shadowtls_config_proto_rawDescGZIP() []byte {
+	file_proxy_shadowtls_config_proto_rawDescOnce.Do(func() {
+		file_proxy_shadowtls_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_shadowtls_config_proto_rawDescData)
+	})
+	return file_proxy_shadowtls_config_proto_rawDescData
+}
+
+var file_proxy_shadowtls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_proxy_shadowtls_config_proto_goTypes = []interface{}{
+	(*ServerConfig)(nil),    // 0: xray.proxy.shadowtls.ServerConfig
+	(*HandshakeConfig)(nil), // 1: xray.proxy.shadowtls.HandshakeConfig
+	(*User)(nil),            // 2: xray.proxy.shadowtls.User
+	(*ClientConfig)(nil),    // 3: xray.proxy.shadowtls.ClientConfig
+	nil,                     // 4: xray.proxy.shadowtls.ServerConfig.HandshakeForServerNameEntry
+	(*net.IPOrDomain)(nil),  // 5: xray.common.net.IPOrDomain
+}
+var file_proxy_shadowtls_config_proto_depIdxs = []int32{
+	2, // 0: xray.proxy.shadowtls.ServerConfig.users:type_name -> xray.proxy.shadowtls.User
+	1, // 1: xray.proxy.shadowtls.ServerConfig.handshake:type_name -> xray.proxy.shadowtls.HandshakeConfig
+	4, // 2: xray.proxy.shadowtls.ServerConfig.handshake_for_server_name:type_name -> xray.proxy.shadowtls.ServerConfig.HandshakeForServerNameEntry
+	5, // 3: xray.proxy.shadowtls.HandshakeConfig.address:type_name -> xray.common.net.IPOrDomain
+	5, // 4: xray.proxy.shadowtls.ClientConfig.address:type_name -> xray.common.net.IPOrDomain
+	1, // 5: xray.proxy.shadowtls.ServerConfig.HandshakeForServerNameEntry.value:type_name -> xray.proxy.shadowtls.HandshakeConfig
+	6, // [6:6] is the sub-list for method output_type
+	6, // [6:6] is the sub-list for method input_type
+	6, // [6:6] is the sub-list for extension type_name
+	6, // [6:6] is the sub-list for extension extendee
+	0, // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_proxy_shadowtls_config_proto_init() }
+func file_proxy_shadowtls_config_proto_init() {
+	if File_proxy_shadowtls_config_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_proxy_shadowtls_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ServerConfig); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_proxy_shadowtls_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HandshakeConfig); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_proxy_shadowtls_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*User); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_proxy_shadowtls_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ClientConfig); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_proxy_shadowtls_config_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   5,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_proxy_shadowtls_config_proto_goTypes,
+		DependencyIndexes: file_proxy_shadowtls_config_proto_depIdxs,
+		MessageInfos:      file_proxy_shadowtls_config_proto_msgTypes,
+	}.Build()
+	File_proxy_shadowtls_config_proto = out.File
+	file_proxy_shadowtls_config_proto_rawDesc = nil
+	file_proxy_shadowtls_config_proto_goTypes = nil
+	file_proxy_shadowtls_config_proto_depIdxs = nil
+}

+ 37 - 0
proxy/shadowtls/config.proto

@@ -0,0 +1,37 @@
+syntax = "proto3";
+
+package xray.proxy.shadowtls;
+option csharp_namespace = "Xray.Proxy.ShadowTLS";
+option go_package = "github.com/xtls/xray-core/proxy/shadowtls";
+option java_package = "com.xray.proxy.shadowtls";
+option java_multiple_files = true;
+
+import "common/net/address.proto";
+
+message ServerConfig {
+  uint32 version = 1;
+  string password = 2;
+  repeated User users = 3;
+  HandshakeConfig handshake = 4;
+  map<string, HandshakeConfig> handshake_for_server_name = 5;
+  bool strict_mode = 6;
+  string detour = 7;
+}
+
+message HandshakeConfig {
+  xray.common.net.IPOrDomain address = 1;
+  uint32 port = 2;
+}
+
+message User {
+  string email = 1;
+  string password = 2;
+  int32 level = 3;
+}
+
+message ClientConfig {
+  xray.common.net.IPOrDomain address = 1;
+  uint32 port = 2;
+  uint32 version = 3;
+  string password = 4;
+}

+ 9 - 0
proxy/shadowtls/errors.generated.go

@@ -0,0 +1,9 @@
+package shadowtls
+
+import "github.com/xtls/xray-core/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 136 - 0
proxy/shadowtls/inbound.go

@@ -0,0 +1,136 @@
+package shadowtls
+
+import (
+	"context"
+	"os"
+
+	"github.com/sagernet/sing-shadowtls"
+	sing_common "github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/log"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/protocol"
+	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
+	"github.com/xtls/xray-core/core"
+	"github.com/xtls/xray-core/features/inbound"
+	"github.com/xtls/xray-core/features/routing"
+	"github.com/xtls/xray-core/proxy"
+	"github.com/xtls/xray-core/transport/internet/stat"
+)
+
+func init() {
+	common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		return NewServer(ctx, config.(*ServerConfig))
+	}))
+}
+
+type Inbound struct {
+	service        *shadowtls.Service
+	inboundManager inbound.Manager
+	detour         string
+}
+
+func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
+	v := core.MustFromContext(ctx)
+	inbound := &Inbound{
+		inboundManager: v.GetFeature(inbound.ManagerType()).(inbound.Manager),
+		detour:         config.Detour,
+	}
+	var handshakeForServerName map[string]shadowtls.HandshakeConfig
+	if config.Version > 1 {
+		handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
+		for serverName, serverConfig := range config.HandshakeForServerName {
+			handshakeForServerName[serverName] = shadowtls.HandshakeConfig{
+				Server: singbridge.ToSocksaddr(net.Destination{
+					Address: serverConfig.Address.AsAddress(),
+					Port:    net.Port(serverConfig.Port),
+				}),
+				Dialer: N.SystemDialer,
+			}
+		}
+	}
+	service, err := shadowtls.NewService(shadowtls.ServiceConfig{
+		Version:  int(config.Version),
+		Password: config.Password,
+		Users: sing_common.Map(config.Users, func(it *User) shadowtls.User {
+			return shadowtls.User{
+				Name:     it.Email,
+				Password: it.Password,
+			}
+		}),
+		Handshake: shadowtls.HandshakeConfig{
+			Server: singbridge.ToSocksaddr(net.Destination{
+				Address: config.Handshake.Address.AsAddress(),
+				Port:    net.Port(config.Handshake.Port),
+			}),
+			Dialer: N.SystemDialer,
+		},
+		HandshakeForServerName: handshakeForServerName,
+		StrictMode:             config.StrictMode,
+		Handler:                inbound,
+		Logger:                 singbridge.NewLogger(newError),
+	})
+	if err != nil {
+		return nil, E.Cause(err, "create service")
+	}
+	inbound.service = service
+	return inbound, nil
+}
+
+func (i *Inbound) Network() []net.Network {
+	return []net.Network{net.Network_TCP}
+}
+
+func (i *Inbound) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error {
+	inbound := session.InboundFromContext(ctx)
+	var metadata M.Metadata
+	if inbound.Source.IsValid() {
+		metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
+	}
+	ctx = session.ContextWithDispatcher(ctx, dispatcher)
+	return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
+}
+
+func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	inboundHandler, err := i.inboundManager.GetHandler(ctx, i.detour)
+	if err != nil {
+		return E.Cause(err, "detour not found")
+	}
+
+	inboundWrapper, loaded := inboundHandler.(proxy.GetInbound)
+	if !loaded {
+		return newError("can't get inbound proxy from handler.")
+	}
+
+	inboundDetour := inboundWrapper.GetInbound()
+
+	email, _ := auth.UserFromContext[string](ctx)
+	inbound := session.InboundFromContext(ctx)
+	inbound.User = &protocol.MemoryUser{
+		Email: email,
+	}
+	ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
+		From:   metadata.Source,
+		To:     metadata.Destination,
+		Status: log.AccessAccepted,
+		Email:  email,
+	})
+	newError("tunnelling request to detour").WriteToLog(session.ExportIDToError(ctx))
+	return inboundDetour.Process(ctx, net.Network_TCP, conn, session.DispatcherFromContext(ctx))
+}
+
+func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
+	return os.ErrInvalid
+}
+
+func (i *Inbound) NewError(ctx context.Context, err error) {
+	if E.IsClosed(err) {
+		return
+	}
+	newError(err).AtWarning().WriteToLog()
+}

+ 83 - 0
proxy/shadowtls/outbound.go

@@ -0,0 +1,83 @@
+package shadowtls
+
+import (
+	"context"
+	"crypto/tls"
+
+	"github.com/sagernet/sing-shadowtls"
+	"github.com/xtls/xray-core/common"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/singbridge"
+	"github.com/xtls/xray-core/transport"
+	"github.com/xtls/xray-core/transport/internet"
+)
+
+func init() {
+	common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+		return NewClient(ctx, config.(*ClientConfig))
+	}))
+}
+
+type Outbound struct {
+	ctx          context.Context
+	clientConfig shadowtls.ClientConfig
+}
+
+func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
+	return &Outbound{
+		ctx: ctx,
+		clientConfig: shadowtls.ClientConfig{
+			Version:  int(config.Version),
+			Password: config.Password,
+			Server: singbridge.ToSocksaddr(net.Destination{
+				Address: config.Address.AsAddress(),
+				Port:    net.Port(config.Port),
+			}),
+			Logger: singbridge.NewLogger(newError),
+		},
+	}, nil
+}
+
+func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
+	var inboundConn net.Conn
+	inbound := session.InboundFromContext(ctx)
+	if inbound != nil {
+		inboundConn = inbound.Conn
+	}
+
+	outbound := session.OutboundFromContext(ctx)
+	if outbound == nil || !outbound.Target.IsValid() {
+		return newError("target not specified")
+	}
+	destination := outbound.Target
+
+	if destination.Network != net.Network_TCP {
+		return newError("only TCP is supported")
+	}
+
+	newError("tunneling request to ", destination, " via ", o.clientConfig.Server).WriteToLog(session.ExportIDToError(ctx))
+
+	var client *shadowtls.Client
+	clientConfig := o.clientConfig
+	if clientConfig.Version == 3 {
+		clientConfig.Dialer = singbridge.NewTLSDialer(dialer, func(conn net.Conn, config *tls.Config) net.Conn {
+			client.SetTLSConfig(config)
+			return conn
+		})
+	} else {
+		clientConfig.Dialer = singbridge.NewDialer(dialer)
+	}
+	var err error
+	client, err = shadowtls.NewClient(clientConfig)
+	if err != nil {
+		return newError("failed to create client").Base(err)
+	}
+
+	conn, err := client.DialContext(ctx)
+	if err != nil {
+		return newError("failed to connect to server").Base(err)
+	}
+
+	return singbridge.CopyConn(ctx, inboundConn, link, conn)
+}

+ 3 - 0
proxy/shadowtls/shadowtls.go

@@ -0,0 +1,3 @@
+package shadowtls
+
+//go:generate go run github.com/xtls/xray-core/common/errors/errorgen

+ 156 - 0
testing/scenarios/shadowtls_test.go

@@ -0,0 +1,156 @@
+package scenarios
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"testing"
+	"time"
+
+	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
+	"github.com/xtls/xray-core/app/log"
+	"github.com/xtls/xray-core/app/proxyman"
+	"github.com/xtls/xray-core/common"
+	clog "github.com/xtls/xray-core/common/log"
+	"github.com/xtls/xray-core/common/net"
+	"github.com/xtls/xray-core/common/serial"
+	"github.com/xtls/xray-core/core"
+	"github.com/xtls/xray-core/proxy/dokodemo"
+	"github.com/xtls/xray-core/proxy/freedom"
+	"github.com/xtls/xray-core/proxy/shadowsocks_2022"
+	"github.com/xtls/xray-core/proxy/shadowtls"
+	"github.com/xtls/xray-core/testing/servers/tcp"
+	"github.com/xtls/xray-core/transport/internet"
+	"github.com/xtls/xray-core/transport/internet/tls"
+	"golang.org/x/sync/errgroup"
+)
+
+func TestShadowTLSTcp(t *testing.T) {
+	password := make([]byte, 32)
+	rand.Read(password)
+	testShadowTLSTcp(t, shadowaead_2022.List[0], base64.StdEncoding.EncodeToString(password))
+}
+
+func testShadowTLSTcp(t *testing.T, method string, password string) {
+	tcpServer := tcp.Server{
+		MsgProcessor: xor,
+	}
+	dest, err := tcpServer.Start()
+	common.Must(err)
+	defer tcpServer.Close()
+
+	serverPort := tcp.PickPort()
+	serverPort = 18462
+	serverConfig := &core.Config{
+		App: []*serial.TypedMessage{
+			serial.ToTypedMessage(&log.Config{
+				ErrorLogLevel: clog.Severity_Debug,
+				ErrorLogType:  log.LogType_Console,
+			}),
+		},
+		Inbound: []*core.InboundHandlerConfig{
+			{
+				ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+					PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
+					Listen:   net.NewIPOrDomain(net.LocalHostIP),
+				}),
+				ProxySettings: serial.ToTypedMessage(&shadowtls.ServerConfig{
+					Version: 3,
+					Users:   []*shadowtls.User{{Password: password}},
+					Handshake: &shadowtls.HandshakeConfig{
+						Address: net.NewIPOrDomain(net.DomainAddress("google.com")),
+						Port:    443,
+					},
+					Detour: "detour",
+				}),
+			},
+			{
+				Tag: "detour",
+				ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+					PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort + 1)}},
+					Listen:   net.NewIPOrDomain(net.LocalHostIP),
+				}),
+				ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{
+					Method:  method,
+					Key:     password,
+					Network: []net.Network{net.Network_TCP},
+				}),
+			},
+		},
+		Outbound: []*core.OutboundHandlerConfig{
+			{
+				ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
+			},
+		},
+	}
+
+	clientPort := tcp.PickPort()
+	clientPort = 12434
+	clientConfig := &core.Config{
+		App: []*serial.TypedMessage{
+			serial.ToTypedMessage(&log.Config{
+				ErrorLogLevel: clog.Severity_Debug,
+				ErrorLogType:  log.LogType_Console,
+			}),
+		},
+		Inbound: []*core.InboundHandlerConfig{
+			{
+				ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+					PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
+					Listen:   net.NewIPOrDomain(net.LocalHostIP),
+				}),
+				ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
+					Address:  net.NewIPOrDomain(dest.Address),
+					Port:     uint32(dest.Port),
+					Networks: []net.Network{net.Network_TCP},
+				}),
+			},
+		},
+		Outbound: []*core.OutboundHandlerConfig{
+			{
+				ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ClientConfig{
+					Address: net.NewIPOrDomain(net.LocalHostIP),
+					Port:    uint32(serverPort),
+					Method:  method,
+					Key:     password,
+				}),
+				SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
+					ProxySettings: &internet.ProxyConfig{
+						Tag: "detour",
+					},
+				}),
+			},
+			{
+				Tag: "detour",
+				ProxySettings: serial.ToTypedMessage(&shadowtls.ClientConfig{
+					Address:  net.NewIPOrDomain(net.LocalHostIP),
+					Port:     uint32(serverPort),
+					Version:  3,
+					Password: password,
+				}),
+				SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
+					StreamSettings: &internet.StreamConfig{
+						SecurityType: serial.GetMessageType(&tls.Config{}),
+						SecuritySettings: []*serial.TypedMessage{
+							serial.ToTypedMessage(&tls.Config{
+								ServerName: "google.com",
+							}),
+						},
+					},
+				}),
+			},
+		},
+	}
+
+	servers, err := InitializeServerConfigs(serverConfig, clientConfig)
+	common.Must(err)
+	defer CloseAllServers(servers)
+
+	var errGroup errgroup.Group
+	for i := 0; i < 10; i++ {
+		errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20))
+	}
+
+	if err := errGroup.Wait(); err != nil {
+		t.Error(err)
+	}
+}

+ 11 - 6
transport/internet/tcp/dialer.go

@@ -23,13 +23,18 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 
 	if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
 		tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
-		if fingerprint := tls.GetFingerprint(config.Fingerprint); fingerprint != nil {
-			conn = tls.UClient(conn, tlsConfig, fingerprint)
-			if err := conn.(*tls.UConn).Handshake(); err != nil {
-				return nil, err
-			}
+		customClient, loaded := tls.CustomClientFromContext(ctx)
+		if loaded {
+			conn = customClient(conn, tlsConfig)
 		} else {
-			conn = tls.Client(conn, tlsConfig)
+			if fingerprint := tls.GetFingerprint(config.Fingerprint); fingerprint != nil {
+				conn = tls.UClient(conn, tlsConfig, fingerprint)
+				if err := conn.(*tls.UConn).Handshake(); err != nil {
+					return nil, err
+				}
+			} else {
+				conn = tls.Client(conn, tlsConfig)
+			}
 		}
 	} else if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil {
 		xtlsConfig := config.GetXTLSConfig(xtls.WithDestination(dest))

+ 21 - 0
transport/internet/tls/custom.go

@@ -0,0 +1,21 @@
+package tls
+
+import (
+	"context"
+	"crypto/tls"
+
+	"github.com/xtls/xray-core/common/net"
+)
+
+type customClientKey struct{}
+
+type CustomClientFunc func(conn net.Conn, config *tls.Config) net.Conn
+
+func CustomClientFromContext(ctx context.Context) (CustomClientFunc, bool) {
+	client, loaded := ctx.Value(customClientKey{}).(CustomClientFunc)
+	return client, loaded
+}
+
+func ContextWithCustomClient(ctx context.Context, customClient CustomClientFunc) context.Context {
+	return context.WithValue(ctx, customClientKey{}, customClient)
+}