浏览代码

Add VLESS server, vision flow and reality TLS

世界 2 年之前
父节点
当前提交
fbc94b9e3e

+ 2 - 2
Makefile

@@ -1,7 +1,7 @@
 NAME = sing-box
 COMMIT = $(shell git rev-parse --short HEAD)
-TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api
-TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr
+TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_reality_server,with_clash_api
+TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
 PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
 MAIN = ./cmd/sing-box
 

+ 7 - 2
common/tls/client.go

@@ -31,6 +31,8 @@ func NewClient(router adapter.Router, serverAddress string, options option.Outbo
 	}
 	if options.ECH != nil && options.ECH.Enabled {
 		return NewECHClient(router, serverAddress, options)
+	} else if options.Reality != nil && options.Reality.Enabled {
+		return NewRealityClient(router, serverAddress, options)
 	} else if options.UTLS != nil && options.UTLS.Enabled {
 		return NewUTLSClient(router, serverAddress, options)
 	} else {
@@ -39,10 +41,13 @@ func NewClient(router adapter.Router, serverAddress string, options option.Outbo
 }
 
 func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
-	tlsConn := config.Client(conn)
 	ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
 	defer cancel()
-	err := tlsConn.HandshakeContext(ctx)
+	tlsConn, err := config.Client(conn)
+	if err != nil {
+		return nil, err
+	}
+	err = tlsConn.HandshakeContext(ctx)
 	if err != nil {
 		return nil, err
 	}

+ 7 - 2
common/tls/config.go

@@ -21,7 +21,7 @@ type Config interface {
 	NextProtos() []string
 	SetNextProtos(nextProto []string)
 	Config() (*STDConfig, error)
-	Client(conn net.Conn) Conn
+	Client(conn net.Conn) (Conn, error)
 	Clone() Config
 }
 
@@ -32,7 +32,12 @@ type ConfigWithSessionIDGenerator interface {
 type ServerConfig interface {
 	Config
 	adapter.Service
-	Server(conn net.Conn) Conn
+	Server(conn net.Conn) (Conn, error)
+}
+
+type ServerConfigCompat interface {
+	ServerConfig
+	ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error)
 }
 
 type Conn interface {

+ 6 - 2
common/tls/ech_client.go

@@ -44,8 +44,8 @@ func (e *ECHClientConfig) Config() (*STDConfig, error) {
 	return nil, E.New("unsupported usage for ECH")
 }
 
-func (e *ECHClientConfig) Client(conn net.Conn) Conn {
-	return &echConnWrapper{cftls.Client(conn, e.config)}
+func (e *ECHClientConfig) Client(conn net.Conn) (Conn, error) {
+	return &echConnWrapper{cftls.Client(conn, e.config)}, nil
 }
 
 func (e *ECHClientConfig) Clone() Config {
@@ -76,6 +76,10 @@ func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
 	}
 }
 
+func (c *echConnWrapper) Upstream() any {
+	return c.Conn
+}
+
 func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
 	var serverName string
 	if options.ServerName != "" {

+ 187 - 0
common/tls/reality_client.go

@@ -0,0 +1,187 @@
+//go:build with_utls
+
+package tls
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/ed25519"
+	"crypto/hmac"
+	"crypto/sha256"
+	"crypto/sha512"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/hex"
+	"fmt"
+	"net"
+	"reflect"
+	"time"
+	"unsafe"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/debug"
+	E "github.com/sagernet/sing/common/exceptions"
+	utls "github.com/sagernet/utls"
+
+	"golang.org/x/crypto/hkdf"
+)
+
+var _ Config = (*RealityClientConfig)(nil)
+
+type RealityClientConfig struct {
+	uClient   *UTLSClientConfig
+	publicKey []byte
+	shortID   []byte
+}
+
+func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
+	if options.UTLS == nil || !options.UTLS.Enabled {
+		return nil, E.New("uTLS is required by reality client")
+	}
+
+	uClient, err := NewUTLSClient(router, serverAddress, options)
+	if err != nil {
+		return nil, err
+	}
+
+	publicKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PublicKey)
+	if err != nil {
+		return nil, E.Cause(err, "decode public_key")
+	}
+	if len(publicKey) != 32 {
+		return nil, E.New("invalid public_key")
+	}
+	shortID, err := hex.DecodeString(options.Reality.ShortID)
+	if err != nil {
+		return nil, E.Cause(err, "decode short_id")
+	}
+	if len(shortID) != 8 {
+		return nil, E.New("invalid short_id")
+	}
+	return &RealityClientConfig{uClient, publicKey, shortID}, nil
+}
+
+func (e *RealityClientConfig) ServerName() string {
+	return e.uClient.ServerName()
+}
+
+func (e *RealityClientConfig) SetServerName(serverName string) {
+	e.uClient.SetServerName(serverName)
+}
+
+func (e *RealityClientConfig) NextProtos() []string {
+	return e.uClient.NextProtos()
+}
+
+func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
+	e.uClient.SetNextProtos(nextProto)
+}
+
+func (e *RealityClientConfig) Config() (*STDConfig, error) {
+	return nil, E.New("unsupported usage for reality")
+}
+
+func (e *RealityClientConfig) Client(conn net.Conn) (Conn, error) {
+	verifier := &realityVerifier{
+		serverName: e.uClient.ServerName(),
+	}
+	uConfig := e.uClient.config.Clone()
+	uConfig.InsecureSkipVerify = true
+	uConfig.SessionTicketsDisabled = true
+	uConfig.VerifyPeerCertificate = verifier.VerifyPeerCertificate
+	uConn := utls.UClient(conn, uConfig, e.uClient.id)
+	verifier.UConn = uConn
+	err := uConn.BuildHandshakeState()
+	if err != nil {
+		return nil, err
+	}
+	hello := uConn.HandshakeState.Hello
+	hello.SessionId = make([]byte, 32)
+	copy(hello.Raw[39:], hello.SessionId)
+
+	var nowTime time.Time
+	if uConfig.Time != nil {
+		nowTime = uConfig.Time()
+	} else {
+		nowTime = time.Now()
+	}
+	binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix()))
+
+	hello.SessionId[0] = 1
+	hello.SessionId[1] = 7
+	hello.SessionId[2] = 5
+	copy(hello.SessionId[8:], e.shortID)
+
+	if debug.Enabled {
+		fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16])
+	}
+
+	authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey)
+	if authKey == nil {
+		return nil, E.New("nil auth_key")
+	}
+	verifier.authKey = authKey
+	_, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey)
+	if err != nil {
+		return nil, err
+	}
+	aesBlock, _ := aes.NewCipher(authKey)
+	aesGcmCipher, _ := cipher.NewGCM(aesBlock)
+	aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)
+	copy(hello.Raw[39:], hello.SessionId)
+	if debug.Enabled {
+		fmt.Printf("REALITY hello.sessionId: %v\n", hello.SessionId)
+		fmt.Printf("REALITY uConn.AuthKey: %v\n", authKey)
+	}
+
+	return &utlsConnWrapper{uConn}, nil
+}
+
+func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
+	e.uClient.config.SessionIDGenerator = generator
+}
+
+func (e *RealityClientConfig) Clone() Config {
+	return &RealityClientConfig{
+		e.uClient.Clone().(*UTLSClientConfig),
+		e.publicKey,
+		e.shortID,
+	}
+}
+
+type realityVerifier struct {
+	*utls.UConn
+	serverName string
+	authKey    []byte
+	verified   bool
+}
+
+func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates")
+	certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset))
+	if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
+		h := hmac.New(sha512.New, c.authKey)
+		h.Write(pub)
+		if bytes.Equal(h.Sum(nil), certs[0].Signature) {
+			c.verified = true
+			return nil
+		}
+	}
+	opts := x509.VerifyOptions{
+		DNSName:       c.serverName,
+		Intermediates: x509.NewCertPool(),
+	}
+	for _, cert := range certs[1:] {
+		opts.Intermediates.AddCert(cert)
+	}
+	if _, err := certs[0].Verify(opts); err != nil {
+		return err
+	}
+	if !c.verified {
+		return E.New("reality verification failed")
+	}
+	return nil
+}

+ 194 - 0
common/tls/reality_server.go

@@ -0,0 +1,194 @@
+//go:build with_reality_server
+
+package tls
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/hex"
+	"net"
+	"os"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/dialer"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common/debug"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+
+	"github.com/nekohasekai/reality"
+)
+
+var _ ServerConfigCompat = (*RealityServerConfig)(nil)
+
+type RealityServerConfig struct {
+	config *reality.Config
+}
+
+func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
+	var tlsConfig reality.Config
+
+	if options.ACME != nil && len(options.ACME.Domain) > 0 {
+		return nil, E.New("acme is unavailable in reality")
+	}
+	tlsConfig.Time = router.TimeFunc()
+	if options.ServerName != "" {
+		tlsConfig.ServerName = options.ServerName
+	}
+	if len(options.ALPN) > 0 {
+		tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
+	}
+	if options.MinVersion != "" {
+		minVersion, err := ParseTLSVersion(options.MinVersion)
+		if err != nil {
+			return nil, E.Cause(err, "parse min_version")
+		}
+		tlsConfig.MinVersion = minVersion
+	}
+	if options.MaxVersion != "" {
+		maxVersion, err := ParseTLSVersion(options.MaxVersion)
+		if err != nil {
+			return nil, E.Cause(err, "parse max_version")
+		}
+		tlsConfig.MaxVersion = maxVersion
+	}
+	if options.CipherSuites != nil {
+	find:
+		for _, cipherSuite := range options.CipherSuites {
+			for _, tlsCipherSuite := range tls.CipherSuites() {
+				if cipherSuite == tlsCipherSuite.Name {
+					tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
+					continue find
+				}
+			}
+			return nil, E.New("unknown cipher_suite: ", cipherSuite)
+		}
+	}
+	if options.Certificate != "" || options.CertificatePath != "" {
+		return nil, E.New("certificate is unavailable in reality")
+	}
+	if options.Key != "" || options.KeyPath != "" {
+		return nil, E.New("key is unavailable in reality")
+	}
+
+	tlsConfig.SessionTicketsDisabled = true
+	tlsConfig.Type = N.NetworkTCP
+	tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String()
+
+	tlsConfig.ServerNames = map[string]bool{options.ServerName: true}
+	privateKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PrivateKey)
+	if err != nil {
+		return nil, E.Cause(err, "decode private key")
+	}
+	if len(privateKey) != 32 {
+		return nil, E.New("invalid private key")
+	}
+	tlsConfig.PrivateKey = privateKey
+	tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
+
+	tlsConfig.ShortIds = make(map[[8]byte]bool)
+	for i, shortID := range options.Reality.ShortID {
+		var shortIDBytesArray [8]byte
+		decodedLen, err := hex.Decode(shortIDBytesArray[:], []byte(shortID))
+		if err != nil {
+			return nil, E.Cause(err, "decode short_id[", i, "]: ", shortID)
+		}
+		if decodedLen != 8 {
+			return nil, E.New("invalid short_id[", i, "]: ", shortID)
+		}
+		tlsConfig.ShortIds[shortIDBytesArray] = true
+	}
+
+	handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions)
+	tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+		return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+	}
+
+	if debug.Enabled {
+		tlsConfig.Show = true
+	}
+
+	return &RealityServerConfig{&tlsConfig}, nil
+}
+
+func (c *RealityServerConfig) ServerName() string {
+	return c.config.ServerName
+}
+
+func (c *RealityServerConfig) SetServerName(serverName string) {
+	c.config.ServerName = serverName
+}
+
+func (c *RealityServerConfig) NextProtos() []string {
+	return c.config.NextProtos
+}
+
+func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
+	c.config.NextProtos = nextProto
+}
+
+func (c *RealityServerConfig) Config() (*tls.Config, error) {
+	return nil, E.New("unsupported usage for reality")
+}
+
+func (c *RealityServerConfig) Client(conn net.Conn) (Conn, error) {
+	return nil, os.ErrInvalid
+}
+
+func (c *RealityServerConfig) Start() error {
+	return nil
+}
+
+func (c *RealityServerConfig) Close() error {
+	return nil
+}
+
+func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) {
+	return nil, os.ErrInvalid
+}
+
+func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
+	tlsConn, err := reality.Server(ctx, conn, c.config)
+	if err != nil {
+		return nil, err
+	}
+	return &realityConnWrapper{Conn: tlsConn}, nil
+}
+
+func (c *RealityServerConfig) Clone() Config {
+	return &RealityServerConfig{
+		config: c.config.Clone(),
+	}
+}
+
+var _ Conn = (*realityConnWrapper)(nil)
+
+type realityConnWrapper struct {
+	*reality.Conn
+}
+
+func (c *realityConnWrapper) ConnectionState() ConnectionState {
+	state := c.Conn.ConnectionState()
+	return tls.ConnectionState{
+		Version:                     state.Version,
+		HandshakeComplete:           state.HandshakeComplete,
+		DidResume:                   state.DidResume,
+		CipherSuite:                 state.CipherSuite,
+		NegotiatedProtocol:          state.NegotiatedProtocol,
+		NegotiatedProtocolIsMutual:  state.NegotiatedProtocolIsMutual,
+		ServerName:                  state.ServerName,
+		PeerCertificates:            state.PeerCertificates,
+		VerifiedChains:              state.VerifiedChains,
+		SignedCertificateTimestamps: state.SignedCertificateTimestamps,
+		OCSPResponse:                state.OCSPResponse,
+		TLSUnique:                   state.TLSUnique,
+	}
+}
+
+func (c *realityConnWrapper) Upstream() any {
+	return c.Conn
+}

+ 16 - 0
common/tls/reality_stub.go

@@ -0,0 +1,16 @@
+//go:build !with_reality_server
+
+package tls
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
+	return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
+}

+ 13 - 3
common/tls/server.go

@@ -16,14 +16,24 @@ func NewServer(ctx context.Context, router adapter.Router, logger log.Logger, op
 	if !options.Enabled {
 		return nil, nil
 	}
-	return NewSTDServer(ctx, router, logger, options)
+	if options.Reality != nil && options.Reality.Enabled {
+		return NewRealityServer(ctx, router, logger, options)
+	} else {
+		return NewSTDServer(ctx, router, logger, options)
+	}
 }
 
 func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
-	tlsConn := config.Server(conn)
 	ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
 	defer cancel()
-	err := tlsConn.HandshakeContext(ctx)
+	if compatServer, isCompat := config.(ServerConfigCompat); isCompat {
+		return compatServer.ServerHandshake(ctx, conn)
+	}
+	tlsConn, err := config.Server(conn)
+	if err != nil {
+		return nil, err
+	}
+	err = tlsConn.HandshakeContext(ctx)
 	if err != nil {
 		return nil, err
 	}

+ 2 - 2
common/tls/std_client.go

@@ -36,8 +36,8 @@ func (s *STDClientConfig) Config() (*STDConfig, error) {
 	return s.config, nil
 }
 
-func (s *STDClientConfig) Client(conn net.Conn) Conn {
-	return tls.Client(conn, s.config)
+func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
+	return tls.Client(conn, s.config), nil
 }
 
 func (s *STDClientConfig) Clone() Config {

+ 4 - 4
common/tls/std_server.go

@@ -48,12 +48,12 @@ func (c *STDServerConfig) Config() (*STDConfig, error) {
 	return c.config, nil
 }
 
-func (c *STDServerConfig) Client(conn net.Conn) Conn {
-	return tls.Client(conn, c.config)
+func (c *STDServerConfig) Client(conn net.Conn) (Conn, error) {
+	return tls.Client(conn, c.config), nil
 }
 
-func (c *STDServerConfig) Server(conn net.Conn) Conn {
-	return tls.Server(conn, c.config)
+func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
+	return tls.Server(conn, c.config), nil
 }
 
 func (c *STDServerConfig) Clone() Config {

+ 31 - 21
common/tls/utls_client.go

@@ -40,14 +40,21 @@ func (e *UTLSClientConfig) Config() (*STDConfig, error) {
 	return nil, E.New("unsupported usage for uTLS")
 }
 
-func (e *UTLSClientConfig) Client(conn net.Conn) Conn {
-	return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}
+func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
+	return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, nil
 }
 
 func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
 	e.config.SessionIDGenerator = generator
 }
 
+func (e *UTLSClientConfig) Clone() Config {
+	return &UTLSClientConfig{
+		config: e.config.Clone(),
+		id:     e.id,
+	}
+}
+
 type utlsConnWrapper struct {
 	*utls.UConn
 }
@@ -70,14 +77,11 @@ func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
 	}
 }
 
-func (e *UTLSClientConfig) Clone() Config {
-	return &UTLSClientConfig{
-		config: e.config.Clone(),
-		id:     e.id,
-	}
+func (c *utlsConnWrapper) Upstream() any {
+	return c.UConn
 }
 
-func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
+func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
 	var serverName string
 	if options.ServerName != "" {
 		serverName = options.ServerName
@@ -148,28 +152,34 @@ func NewUTLSClient(router adapter.Router, serverAddress string, options option.O
 		}
 		tlsConfig.RootCAs = certPool
 	}
-	var id utls.ClientHelloID
-	switch options.UTLS.Fingerprint {
+	id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
+	if err != nil {
+		return nil, err
+	}
+	return &UTLSClientConfig{&tlsConfig, id}, nil
+}
+
+func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
+	switch name {
 	case "chrome", "":
-		id = utls.HelloChrome_Auto
+		return utls.HelloChrome_Auto, nil
 	case "firefox":
-		id = utls.HelloFirefox_Auto
+		return utls.HelloFirefox_Auto, nil
 	case "edge":
-		id = utls.HelloEdge_Auto
+		return utls.HelloEdge_Auto, nil
 	case "safari":
-		id = utls.HelloSafari_Auto
+		return utls.HelloSafari_Auto, nil
 	case "360":
-		id = utls.Hello360_Auto
+		return utls.Hello360_Auto, nil
 	case "qq":
-		id = utls.HelloQQ_Auto
+		return utls.HelloQQ_Auto, nil
 	case "ios":
-		id = utls.HelloIOS_Auto
+		return utls.HelloIOS_Auto, nil
 	case "android":
-		id = utls.HelloAndroid_11_OkHttp
+		return utls.HelloAndroid_11_OkHttp, nil
 	case "random":
-		id = utls.HelloRandomized
+		return utls.HelloRandomized, nil
 	default:
-		return nil, E.New("unknown uTLS fingerprint: ", options.UTLS.Fingerprint)
+		return utls.ClientHelloID{}, E.New("unknown uTLS fingerprint: ", name)
 	}
-	return &UTLSClientConfig{&tlsConfig, id}, nil
 }

+ 4 - 0
common/tls/utls_stub.go

@@ -11,3 +11,7 @@ import (
 func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
 	return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
 }
+
+func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
+	return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
+}

+ 1 - 0
docs/configuration/inbound/index.md

@@ -27,6 +27,7 @@
 | `naive`       | [Naive](./naive)             | X          |
 | `hysteria`    | [Hysteria](./hysteria)       | X          |
 | `shadowtls`   | [ShadowTLS](./shadowtls)     | TCP        |
+| `vless`       | [VLESS](./vless)             | TCP        |
 | `tun`         | [Tun](./tun)                 | X          |
 | `redirect`    | [Redirect](./redirect)       | X          |
 | `tproxy`      | [TProxy](./tproxy)           | X          |

+ 39 - 0
docs/configuration/inbound/vless.md

@@ -0,0 +1,39 @@
+### Structure
+
+```json
+{
+  "type": "vless",
+  "tag": "vless-in",
+
+  ... // Listen Fields
+
+  "users": [
+    {
+      "name": "sekai",
+      "uuid": "bf000d23-0752-40b4-affe-68f7707a9661"
+    }
+  ],
+  "tls": {},
+  "transport": {}
+}
+```
+
+### Listen Fields
+
+See [Listen Fields](/configuration/shared/listen) for details.
+
+### Fields
+
+#### users
+
+==Required==
+
+VLESS users.
+
+#### tls
+
+TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
+
+#### transport
+
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).

+ 39 - 0
docs/configuration/inbound/vless.zh.md

@@ -0,0 +1,39 @@
+### 结构
+
+```json
+{
+  "type": "vless",
+  "tag": "vless-in",
+
+  ... // 监听字段
+
+  "users": [
+    {
+      "name": "sekai",
+      "uuid": "bf000d23-0752-40b4-affe-68f7707a9661"
+    }
+  ],
+  "tls": {},
+  "transport": {}
+}
+```
+
+### 监听字段
+
+参阅 [监听字段](/zh/configuration/shared/listen/)。
+
+### 字段
+
+#### users
+
+==必填==
+
+VLESS 用户。
+
+#### tls
+
+TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
+
+#### transport
+
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。

+ 9 - 4
docs/configuration/outbound/vless.md

@@ -8,6 +8,7 @@
   "server": "127.0.0.1",
   "server_port": 1080,
   "uuid": "bf000d23-0752-40b4-affe-68f7707a9661",
+  "flow": "xtls-rprx-vision",
   "network": "tcp",
   "tls": {},
   "packet_encoding": "",
@@ -17,10 +18,6 @@
 }
 ```
 
-!!! warning ""
-
-    The VLESS protocol is architecturally coupled to v2ray and is unmaintained. This outbound is provided for compatibility purposes only.
-
 ### Fields
 
 #### server
@@ -41,6 +38,14 @@ The server port.
 
 The VLESS user id.
 
+#### flow
+
+VLESS Sub-protocol.
+
+Available values:
+
+* `xtls-rprx-vision`
+
 #### network
 
 Enabled network

+ 9 - 4
docs/configuration/outbound/vless.zh.md

@@ -8,6 +8,7 @@
   "server": "127.0.0.1",
   "server_port": 1080,
   "uuid": "bf000d23-0752-40b4-affe-68f7707a9661",
+  "flow": "xtls-rprx-vision",
   "network": "tcp",
   "tls": {},
   "packet_encoding": "",
@@ -17,10 +18,6 @@
 }
 ```
 
-!!! warning ""
-
-    VLESS 协议与 v2ray 架构耦合且无人维护。 提供此出站仅出于兼容性目的。
-
 ### 字段
 
 #### server
@@ -41,6 +38,14 @@
 
 VLESS 用户 ID。
 
+#### flow
+
+VLESS 子协议。
+
+可用值:
+
+* `xtls-rprx-vision`
+
 #### network
 
 启用的网络协议。

+ 67 - 0
docs/configuration/shared/tls.md

@@ -26,6 +26,20 @@
       "key_id": "",
       "mac_key": ""
     }
+  },
+  "reality": {
+    "enabled": false,
+    "handshake": {
+      "server": "google.com",
+      "server_port": 443,
+
+      ... // Dial Fields
+    },
+    "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+    "short_id": [
+      "0123456789abcdef"
+    ],
+    "max_time_difference": "1m"
   }
 }
 ```
@@ -53,6 +67,11 @@
   "utls": {
     "enabled": false,
     "fingerprint": ""
+  },
+  "reality": {
+    "enabled": false,
+    "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+    "short_id": "0123456789abcdef"
   }
 }
 ```
@@ -275,6 +294,54 @@ The key identifier.
 
 The MAC key.
 
+### Reality Fields
+
+!!! warning ""
+
+    reality server is not included by default, see [Installation](/#installation).
+
+!!! warning ""
+
+    uTLS, which is required by reality client is not included by default, see [Installation](/#installation).
+
+#### handshake
+
+==Server only==
+
+==Required==
+
+Handshake server address and [Dial options](/configuration/shared/dial).
+
+#### private_key
+
+==Server only==
+
+==Required==
+
+Private key, generated by `./xray x25519`.
+
+#### public_key
+
+==Client only==
+
+==Required==
+
+Public key, generated by `./xray x25519`.
+
+#### short_id
+
+==Required==
+
+A 8-bit hex string.
+
+#### max_time_difference
+
+==Server only==
+
+The maximum time difference between the server and the client.
+
+Check disabled if empty.
+
 ### Reload
 
 For server configuration, certificate and key will be automatically reloaded if modified.

+ 65 - 0
docs/configuration/shared/tls.zh.md

@@ -26,6 +26,20 @@
       "key_id": "",
       "mac_key": ""
     }
+  },
+  "reality": {
+    "enabled": false,
+    "handshake": {
+      "server": "google.com",
+      "server_port": 443,
+
+      ... // 拨号字段
+    },
+    "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+    "short_id": [
+      "0123456789abcdef"
+    ],
+    "max_time_difference": "1m"
   }
 }
 ```
@@ -53,6 +67,11 @@
   "utls": {
     "enabled": false,
     "fingerprint": ""
+  },
+  "reality": {
+    "enabled": false,
+    "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+    "short_id": "0123456789abcdef"
   }
 }
 ```
@@ -271,6 +290,52 @@ EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到其他已知
 
 MAC 密钥。
 
+### Reality 字段
+
+!!! warning ""
+
+    默认安装不包含 reality 服务器,参阅 [安装](/zh/#_2)。
+
+!!! warning ""
+
+    默认安装不包含被 reality 客户端需要的 uTLS, 参阅 [安装](/zh/#_2)。
+
+#### handshake
+
+==仅服务器==
+
+==必填==
+
+握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。
+
+#### private_key
+
+==仅服务器==
+
+==必填==
+
+私钥,由 `./xray x25519` 生成。
+
+#### public_key
+
+==仅客户端==
+
+==必填==
+
+公钥,由 `./xray x25519` 生成。
+
+#### short_id
+
+==必填==
+
+一个八位十六进制的字符串。
+
+#### max_time_difference
+
+服务器与和客户端之间允许的最大时间差。
+
+默认禁用检查。
+
 ### 重载
 
 对于服务器配置,如果修改,证书和密钥将自动重新加载。

+ 2 - 1
go.mod

@@ -18,6 +18,7 @@ require (
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/mholt/acmez v1.1.0
 	github.com/miekg/dns v1.1.50
+	github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd
 	github.com/oschwald/maxminddb-golang v1.10.0
 	github.com/pires/go-proxyproto v0.6.2
 	github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
@@ -31,7 +32,7 @@ require (
 	github.com/sagernet/sing-vmess v0.1.2
 	github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195
 	github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d
-	github.com/sagernet/utls v0.0.0-20230220130002-c08891932056
+	github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01
 	github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e
 	github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c
 	github.com/spf13/cobra v1.6.1

+ 4 - 2
go.sum

@@ -92,6 +92,8 @@ github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY=
 github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs=
 github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
 github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd h1:vd4qbG9ZTW10e1uqo8PDLshe5XL2yPhdINhGlJYaOoQ=
+github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd/go.mod h1:C+iqSNDBQ8qMhlNZ0JSUO9POEWq8qX87hukGfmO7/fA=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
@@ -143,8 +145,8 @@ github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT
 github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8=
 github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk=
 github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g=
-github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 h1:gDXi/0uYe8dA48UyUI1LM2la5QYN0IvsDvR2H2+kFnA=
-github.com/sagernet/utls v0.0.0-20230220130002-c08891932056/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
+github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 h1:m4MI13+NRKddIvbdSN0sFHK8w5ROTa60Zi9diZ7EE08=
+github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
 github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs=
 github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY=
 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=

+ 2 - 0
inbound/builder.go

@@ -42,6 +42,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
 	case C.TypeShadowTLS:
 		return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
+	case C.TypeVLESS:
+		return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
 	default:
 		return nil, E.New("unknown inbound type: ", options.Type)
 	}

+ 193 - 0
inbound/vless.go

@@ -0,0 +1,193 @@
+package inbound
+
+import (
+	"context"
+	"net"
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tls"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/v2ray"
+	"github.com/sagernet/sing-box/transport/vless"
+	"github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing-vmess/packetaddr"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var (
+	_ adapter.Inbound           = (*VLESS)(nil)
+	_ adapter.InjectableInbound = (*VLESS)(nil)
+)
+
+type VLESS struct {
+	myInboundAdapter
+	ctx       context.Context
+	users     []option.VLESSUser
+	service   *vless.Service[int]
+	tlsConfig tls.ServerConfig
+	transport adapter.V2RayServerTransport
+}
+
+func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (*VLESS, error) {
+	inbound := &VLESS{
+		myInboundAdapter: myInboundAdapter{
+			protocol:      C.TypeVLESS,
+			network:       []string{N.NetworkTCP},
+			ctx:           ctx,
+			router:        router,
+			logger:        logger,
+			tag:           tag,
+			listenOptions: options.ListenOptions,
+		},
+		ctx:   ctx,
+		users: options.Users,
+	}
+	service := vless.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
+	service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
+		return index
+	}), common.Map(inbound.users, func(it option.VLESSUser) string {
+		return it.UUID
+	}))
+	inbound.service = service
+	var err error
+	if options.TLS != nil {
+		inbound.tlsConfig, err = tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
+		if err != nil {
+			return nil, err
+		}
+	}
+	if options.Transport != nil {
+		inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound))
+		if err != nil {
+			return nil, E.Cause(err, "create server transport: ", options.Transport.Type)
+		}
+	}
+	inbound.connHandler = inbound
+	return inbound, nil
+}
+
+func (h *VLESS) Start() error {
+	err := common.Start(
+		h.service,
+		h.tlsConfig,
+	)
+	if err != nil {
+		return err
+	}
+	if h.transport == nil {
+		return h.myInboundAdapter.Start()
+	}
+	if common.Contains(h.transport.Network(), N.NetworkTCP) {
+		tcpListener, err := h.myInboundAdapter.ListenTCP()
+		if err != nil {
+			return err
+		}
+		go func() {
+			sErr := h.transport.Serve(tcpListener)
+			if sErr != nil && !E.IsClosed(sErr) {
+				h.logger.Error("transport serve error: ", sErr)
+			}
+		}()
+	}
+	if common.Contains(h.transport.Network(), N.NetworkUDP) {
+		udpConn, err := h.myInboundAdapter.ListenUDP()
+		if err != nil {
+			return err
+		}
+		go func() {
+			sErr := h.transport.ServePacket(udpConn)
+			if sErr != nil && !E.IsClosed(sErr) {
+				h.logger.Error("transport serve error: ", sErr)
+			}
+		}()
+	}
+	return nil
+}
+
+func (h *VLESS) Close() error {
+	return common.Close(
+		h.service,
+		&h.myInboundAdapter,
+		h.tlsConfig,
+		h.transport,
+	)
+}
+
+func (h *VLESS) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	h.injectTCP(conn, metadata)
+	return nil
+}
+
+func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	var err error
+	if h.tlsConfig != nil && h.transport == nil {
+		conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig)
+		if err != nil {
+			return err
+		}
+	}
+	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
+}
+
+func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return os.ErrInvalid
+}
+
+func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	userIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	user := h.users[userIndex].Name
+	if user == "" {
+		user = F.ToString(userIndex)
+	} else {
+		metadata.User = user
+	}
+	h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
+	return h.router.RouteConnection(ctx, conn, metadata)
+}
+
+func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	userIndex, loaded := auth.UserFromContext[int](ctx)
+	if !loaded {
+		return os.ErrInvalid
+	}
+	user := h.users[userIndex].Name
+	if user == "" {
+		user = F.ToString(userIndex)
+	} else {
+		metadata.User = user
+	}
+	if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress {
+		metadata.Destination = M.Socksaddr{}
+		conn = packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination)
+		h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection")
+	} else {
+		h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
+	}
+	return h.router.RoutePacketConnection(ctx, conn, metadata)
+}
+
+var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil)
+
+type vlessTransportHandler VLESS
+
+func (t *vlessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	return (*VLESS)(t).newTransportConnection(ctx, conn, adapter.InboundContext{
+		Source:      metadata.Source,
+		Destination: metadata.Destination,
+	})
+}
+
+func (t *vlessTransportHandler) FallbackConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	return os.ErrInvalid
+}

+ 1 - 0
mkdocs.yml

@@ -71,6 +71,7 @@ nav:
           - Naive: configuration/inbound/naive.md
           - Hysteria: configuration/inbound/hysteria.md
           - ShadowTLS: configuration/inbound/shadowtls.md
+          - VLESS: configuration/inbound/vless.md
           - Tun: configuration/inbound/tun.md
           - Redirect: configuration/inbound/redirect.md
           - TProxy: configuration/inbound/tproxy.md

+ 5 - 0
option/inbound.go

@@ -22,6 +22,7 @@ type _Inbound struct {
 	NaiveOptions       NaiveInboundOptions       `json:"-"`
 	HysteriaOptions    HysteriaInboundOptions    `json:"-"`
 	ShadowTLSOptions   ShadowTLSInboundOptions   `json:"-"`
+	VLESSOptions       VLESSInboundOptions       `json:"-"`
 }
 
 type Inbound _Inbound
@@ -55,6 +56,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 		v = h.HysteriaOptions
 	case C.TypeShadowTLS:
 		v = h.ShadowTLSOptions
+	case C.TypeVLESS:
+		v = h.VLESSOptions
 	default:
 		return nil, E.New("unknown inbound type: ", h.Type)
 	}
@@ -94,6 +97,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.HysteriaOptions
 	case C.TypeShadowTLS:
 		v = &h.ShadowTLSOptions
+	case C.TypeVLESS:
+		v = &h.VLESSOptions
 	default:
 		return E.New("unknown inbound type: ", h.Type)
 	}

+ 45 - 24
option/tls.go

@@ -1,33 +1,48 @@
 package option
 
 type InboundTLSOptions struct {
-	Enabled         bool                `json:"enabled,omitempty"`
-	ServerName      string              `json:"server_name,omitempty"`
-	Insecure        bool                `json:"insecure,omitempty"`
-	ALPN            Listable[string]    `json:"alpn,omitempty"`
-	MinVersion      string              `json:"min_version,omitempty"`
-	MaxVersion      string              `json:"max_version,omitempty"`
-	CipherSuites    Listable[string]    `json:"cipher_suites,omitempty"`
-	Certificate     string              `json:"certificate,omitempty"`
-	CertificatePath string              `json:"certificate_path,omitempty"`
-	Key             string              `json:"key,omitempty"`
-	KeyPath         string              `json:"key_path,omitempty"`
-	ACME            *InboundACMEOptions `json:"acme,omitempty"`
+	Enabled         bool                   `json:"enabled,omitempty"`
+	ServerName      string                 `json:"server_name,omitempty"`
+	Insecure        bool                   `json:"insecure,omitempty"`
+	ALPN            Listable[string]       `json:"alpn,omitempty"`
+	MinVersion      string                 `json:"min_version,omitempty"`
+	MaxVersion      string                 `json:"max_version,omitempty"`
+	CipherSuites    Listable[string]       `json:"cipher_suites,omitempty"`
+	Certificate     string                 `json:"certificate,omitempty"`
+	CertificatePath string                 `json:"certificate_path,omitempty"`
+	Key             string                 `json:"key,omitempty"`
+	KeyPath         string                 `json:"key_path,omitempty"`
+	ACME            *InboundACMEOptions    `json:"acme,omitempty"`
+	Reality         *InboundRealityOptions `json:"reality,omitempty"`
 }
 
 type OutboundTLSOptions struct {
-	Enabled         bool                 `json:"enabled,omitempty"`
-	DisableSNI      bool                 `json:"disable_sni,omitempty"`
-	ServerName      string               `json:"server_name,omitempty"`
-	Insecure        bool                 `json:"insecure,omitempty"`
-	ALPN            Listable[string]     `json:"alpn,omitempty"`
-	MinVersion      string               `json:"min_version,omitempty"`
-	MaxVersion      string               `json:"max_version,omitempty"`
-	CipherSuites    Listable[string]     `json:"cipher_suites,omitempty"`
-	Certificate     string               `json:"certificate,omitempty"`
-	CertificatePath string               `json:"certificate_path,omitempty"`
-	ECH             *OutboundECHOptions  `json:"ech,omitempty"`
-	UTLS            *OutboundUTLSOptions `json:"utls,omitempty"`
+	Enabled         bool                    `json:"enabled,omitempty"`
+	DisableSNI      bool                    `json:"disable_sni,omitempty"`
+	ServerName      string                  `json:"server_name,omitempty"`
+	Insecure        bool                    `json:"insecure,omitempty"`
+	ALPN            Listable[string]        `json:"alpn,omitempty"`
+	MinVersion      string                  `json:"min_version,omitempty"`
+	MaxVersion      string                  `json:"max_version,omitempty"`
+	CipherSuites    Listable[string]        `json:"cipher_suites,omitempty"`
+	Certificate     string                  `json:"certificate,omitempty"`
+	CertificatePath string                  `json:"certificate_path,omitempty"`
+	ECH             *OutboundECHOptions     `json:"ech,omitempty"`
+	UTLS            *OutboundUTLSOptions    `json:"utls,omitempty"`
+	Reality         *OutboundRealityOptions `json:"reality,omitempty"`
+}
+
+type InboundRealityOptions struct {
+	Enabled           bool                           `json:"enabled,omitempty"`
+	Handshake         InboundRealityHandshakeOptions `json:"handshake,omitempty"`
+	PrivateKey        string                         `json:"private_key,omitempty"`
+	ShortID           Listable[string]               `json:"short_id,omitempty"`
+	MaxTimeDifference Duration                       `json:"max_time_difference,omitempty"`
+}
+
+type InboundRealityHandshakeOptions struct {
+	ServerOptions
+	DialerOptions
 }
 
 type OutboundECHOptions struct {
@@ -41,3 +56,9 @@ type OutboundUTLSOptions struct {
 	Enabled     bool   `json:"enabled,omitempty"`
 	Fingerprint string `json:"fingerprint,omitempty"`
 }
+
+type OutboundRealityOptions struct {
+	Enabled   bool   `json:"enabled,omitempty"`
+	PublicKey string `json:"public_key,omitempty"`
+	ShortID   string `json:"short_id,omitempty"`
+}

+ 13 - 0
option/vless.go

@@ -1,9 +1,22 @@
 package option
 
+type VLESSInboundOptions struct {
+	ListenOptions
+	Users     []VLESSUser            `json:"users,omitempty"`
+	TLS       *InboundTLSOptions     `json:"tls,omitempty"`
+	Transport *V2RayTransportOptions `json:"transport,omitempty"`
+}
+
+type VLESSUser struct {
+	Name string `json:"name"`
+	UUID string `json:"uuid"`
+}
+
 type VLESSOutboundOptions struct {
 	DialerOptions
 	ServerOptions
 	UUID           string                 `json:"uuid"`
+	Flow           string                 `json:"flow,omitempty"`
 	Network        NetworkList            `json:"network,omitempty"`
 	TLS            *OutboundTLSOptions    `json:"tls,omitempty"`
 	Transport      *V2RayTransportOptions `json:"transport,omitempty"`

+ 11 - 11
outbound/vless.go

@@ -11,9 +11,9 @@ import (
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2ray"
+	"github.com/sagernet/sing-box/transport/vless"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-vmess/packetaddr"
-	"github.com/sagernet/sing-vmess/vless"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
@@ -67,7 +67,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 	default:
 		return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
 	}
-	outbound.client, err = vless.NewClient(options.UUID)
+	outbound.client, err = vless.NewClient(options.UUID, options.Flow)
 	if err != nil {
 		return nil, err
 	}
@@ -92,16 +92,12 @@ func (h *VLESS) DialContext(ctx context.Context, network string, destination M.S
 		return nil, err
 	}
 	switch N.NetworkName(network) {
-	case N.NetworkTCP:
-	case N.NetworkUDP:
-	}
-	switch N.NetworkName(network) {
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
-		return h.client.DialEarlyConn(conn, destination), nil
+		return h.client.DialEarlyConn(conn, destination)
 	case N.NetworkUDP:
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
-		return h.client.DialEarlyPacketConn(conn, destination), nil
+		return h.client.DialEarlyPacketConn(conn, destination)
 	default:
 		return nil, E.Extend(N.ErrUnknownNetwork, network)
 	}
@@ -126,11 +122,15 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 		return nil, err
 	}
 	if h.xudp {
-		return h.client.DialEarlyXUDPPacketConn(conn, destination), nil
+		return h.client.DialEarlyXUDPPacketConn(conn, destination)
 	} else if h.packetAddr {
-		return dialer.NewResolvePacketConn(ctx, h.router, dns.DomainStrategyAsIS, packetaddr.NewConn(h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination)), nil
+		conn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress})
+		if err != nil {
+			return nil, err
+		}
+		return dialer.NewResolvePacketConn(ctx, h.router, dns.DomainStrategyAsIS, packetaddr.NewConn(conn, destination)), nil
 	} else {
-		return h.client.DialEarlyPacketConn(conn, destination), nil
+		return h.client.DialEarlyPacketConn(conn, destination)
 	}
 }
 

+ 0 - 9
test/clash_test.go

@@ -13,7 +13,6 @@ import (
 	"testing"
 	"time"
 
-	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common/control"
 	F "github.com/sagernet/sing/common/format"
@@ -59,14 +58,6 @@ var allImages = []string{
 var localIP = netip.MustParseAddr("127.0.0.1")
 
 func init() {
-	if C.IsDarwin {
-		var err error
-		localIP, err = defaultRouteIP()
-		if err != nil {
-			panic(err)
-		}
-	}
-
 	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	if err != nil {
 		panic(err)

+ 39 - 0
test/config/vless-tls-server.json

@@ -0,0 +1,39 @@
+{
+  "log": {
+    "loglevel": "debug"
+  },
+  "inbounds": [
+    {
+      "listen": "0.0.0.0",
+      "port": 1234,
+      "protocol": "vless",
+      "settings": {
+        "decryption": "none",
+        "clients": [
+          {
+            "id": "b831381d-6324-4d53-ad4f-8cda48b30811",
+            "flow": ""
+          }
+        ]
+      },
+      "streamSettings": {
+        "network": "tcp",
+        "security": "tls",
+        "tlsSettings": {
+          "serverName": "example.org",
+          "certificates": [
+            {
+              "certificateFile": "/path/to/certificate.crt",
+              "keyFile": "/path/to/private.key"
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "outbounds": [
+    {
+      "protocol": "freedom"
+    }
+  ]
+}

+ 5 - 2
test/go.mod

@@ -18,6 +18,8 @@ require (
 	golang.org/x/net v0.7.0
 )
 
+replace github.com/xtls/reality => github.com/nekohasekai/reality v0.0.0-20230225043811-04070a6bdbea
+
 require (
 	berty.tech/go-libtor v1.0.385 // indirect
 	github.com/Dreamacro/clash v1.13.0 // indirect
@@ -51,6 +53,7 @@ require (
 	github.com/miekg/dns v1.1.50 // indirect
 	github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
+	github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd // indirect
 	github.com/onsi/ginkgo/v2 v2.2.0 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.2 // indirect
@@ -68,12 +71,12 @@ require (
 	github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
 	github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 // indirect
 	github.com/sagernet/sing-dns v0.1.4 // indirect
-	github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260 // indirect
+	github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587 // indirect
 	github.com/sagernet/sing-tun v0.1.1 // indirect
 	github.com/sagernet/sing-vmess v0.1.2 // indirect
 	github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 // indirect
 	github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d // indirect
-	github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 // indirect
+	github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 // indirect
 	github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect
 	github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect

+ 6 - 4
test/go.sum

@@ -104,6 +104,8 @@ github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/PO
 github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd h1:vd4qbG9ZTW10e1uqo8PDLshe5XL2yPhdINhGlJYaOoQ=
+github.com/nekohasekai/reality v0.0.0-20230225080858-d70c703b04cd/go.mod h1:C+iqSNDBQ8qMhlNZ0JSUO9POEWq8qX87hukGfmO7/fA=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
 github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
@@ -146,8 +148,8 @@ github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN
 github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9/go.mod h1:f3mHTy5shnVM9l8UocMlJgC/1G/zdj5FuEuVXhDinGU=
-github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260 h1:RKeyBMI5kRnno3/WGsW4HrGnZkhISQQrnRxAKXbf5Vg=
-github.com/sagernet/sing-shadowtls v0.0.0-20230221075551-c5ad05179260/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587 h1:OjIXlHT2bblZfp+ciupM4xY9+Ccpj9FsuHRtKRBv+Pg=
+github.com/sagernet/sing-shadowtls v0.0.0-20230221123345-78e50cd7b587/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
 github.com/sagernet/sing-tun v0.1.1 h1:2Hg3GAyJWzQ7Ua1j74dE+mI06vaqSBO9yD4tkTjggn4=
 github.com/sagernet/sing-tun v0.1.1/go.mod h1:WzW/SkT+Nh9uJn/FIYUE2YJHYuPwfbh8sATOzU9QDGw=
 github.com/sagernet/sing-vmess v0.1.2 h1:RbOZNAId2LrCai8epMoQXlf0XTrou0bfcw08hNBg6lM=
@@ -156,8 +158,8 @@ github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT
 github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195/go.mod h1:yedWtra8nyGJ+SyI+ziwuaGMzBatbB10P1IOOZbbSK8=
 github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk=
 github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g=
-github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 h1:gDXi/0uYe8dA48UyUI1LM2la5QYN0IvsDvR2H2+kFnA=
-github.com/sagernet/utls v0.0.0-20230220130002-c08891932056/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
+github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01 h1:m4MI13+NRKddIvbdSN0sFHK8w5ROTa60Zi9diZ7EE08=
+github.com/sagernet/utls v0.0.0-20230225061716-536a007c8b01/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
 github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs=
 github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY=
 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=

+ 420 - 12
test/vless_test.go

@@ -7,6 +7,7 @@ import (
 
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/vless"
 
 	"github.com/spyzhov/ajson"
 	"github.com/stretchr/testify/require"
@@ -28,8 +29,9 @@ func TestVLESS(t *testing.T) {
 
 	startDockerContainer(t, DockerOptions{
 		Image:      ImageV2RayCore,
-		Ports:      []uint16{serverPort, testPort},
+		Ports:      []uint16{serverPort},
 		EntryPoint: "v2ray",
+		Cmd:        []string{"run"},
 		Stdin:      content,
 	})
 
@@ -58,36 +60,48 @@ func TestVLESS(t *testing.T) {
 			},
 		},
 	})
-	testSuit(t, clientPort, testPort)
+	testTCP(t, clientPort, testPort)
 }
 
 func TestVLESSXRay(t *testing.T) {
-	testVLESSXray(t, "")
+	t.Run("origin", func(t *testing.T) {
+		testVLESSXray(t, "", "")
+	})
+	t.Run("xudp", func(t *testing.T) {
+		testVLESSXray(t, "xudp", "")
+	})
+	t.Run("vision", func(t *testing.T) {
+		testVLESSXray(t, "", vless.FlowVision)
+	})
 }
 
-func TestVLESSXUDP(t *testing.T) {
-	testVLESSXray(t, "xudp")
-}
+func testVLESSXray(t *testing.T, packetEncoding string, flow string) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
 
-func testVLESSXray(t *testing.T, packetEncoding string) {
-	content, err := os.ReadFile("config/vless-server.json")
+	content, err := os.ReadFile("config/vless-tls-server.json")
 	require.NoError(t, err)
 	config, err := ajson.Unmarshal(content)
 	require.NoError(t, err)
 
-	user := newUUID()
+	userID := newUUID()
 	inbound := config.MustKey("inbounds").MustIndex(0)
 	inbound.MustKey("port").SetNumeric(float64(serverPort))
-	inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String())
+	user := inbound.MustKey("settings").MustKey("clients").MustIndex(0)
+	user.MustKey("id").SetString(userID.String())
+	user.MustKey("flow").SetString(flow)
 
 	content, err = ajson.Marshal(config)
 	require.NoError(t, err)
 
 	startDockerContainer(t, DockerOptions{
 		Image:      ImageXRayCore,
-		Ports:      []uint16{serverPort, testPort},
+		Ports:      []uint16{serverPort},
 		EntryPoint: "xray",
 		Stdin:      content,
+		Bind: map[string]string{
+			certPem: "/path/to/certificate.crt",
+			keyPem:  "/path/to/private.key",
+		},
 	})
 
 	startInstance(t, option.Options{
@@ -101,17 +115,411 @@ func testVLESSXray(t *testing.T, packetEncoding string) {
 					},
 				},
 			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan",
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Users: []option.TrojanUser{
+						{
+							Name:     "sekai",
+							Password: userID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
 		},
 		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeTrojan,
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "host.docker.internal",
+						ServerPort: otherPort,
+					},
+					Password: userID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					DialerOptions: option.DialerOptions{
+						Detour: "vless",
+					},
+				},
+			},
 			{
 				Type: C.TypeVLESS,
+				Tag:  "vless",
 				VLESSOptions: option.VLESSOutboundOptions{
 					ServerOptions: option.ServerOptions{
 						Server:     "127.0.0.1",
 						ServerPort: serverPort,
 					},
-					UUID:           user.String(),
+					UUID:           userID.String(),
+					Flow:           flow,
 					PacketEncoding: packetEncoding,
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+				},
+			},
+			{
+				Type: C.TypeDirect,
+				Tag:  "direct",
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"trojan"},
+						Outbound: "direct",
+					},
+				},
+			},
+		},
+	})
+
+	testTCP(t, clientPort, testPort)
+}
+
+func TestVLESSSelf(t *testing.T) {
+	t.Run("origin", func(t *testing.T) {
+		testVLESSSelf(t, "")
+	})
+	t.Run("vision", func(t *testing.T) {
+		testVLESSSelf(t, vless.FlowVision)
+	})
+	t.Run("vision-tls", func(t *testing.T) {
+		testVLESSSelfTLS(t, vless.FlowVision)
+	})
+}
+
+func testVLESSSelf(t *testing.T, flow string) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	userUUID := newUUID()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{
+						{
+							Name: "sekai",
+							UUID: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "vless-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: userUUID.String(),
+					Flow: flow,
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "vless-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func testVLESSSelfTLS(t *testing.T, flow string) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	userUUID := newUUID()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{
+						{
+							Name: "sekai",
+							UUID: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan",
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Users: []option.TrojanUser{
+						{
+							Name:     "sekai",
+							Password: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan-out",
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: otherPort,
+					},
+					Password: userUUID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					DialerOptions: option.DialerOptions{
+						Detour: "vless-out",
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "vless-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: userUUID.String(),
+					Flow: flow,
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "trojan-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestVLESSVisionReality(t *testing.T) {
+	_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+
+	userUUID := newUUID()
+	startInstance(t, option.Options{
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				VLESSOptions: option.VLESSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Users: []option.VLESSUser{
+						{
+							Name: "sekai",
+							UUID: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.InboundRealityOptions{
+							Enabled: true,
+							Handshake: option.InboundRealityHandshakeOptions{
+								ServerOptions: option.ServerOptions{
+									Server:     "google.com",
+									ServerPort: 443,
+								},
+							},
+							ShortID:    []string{"0123456789abcdef"},
+							PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+						},
+					},
+				},
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan",
+				TrojanOptions: option.TrojanInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Users: []option.TrojanUser{
+						{
+							Name:     "sekai",
+							Password: userUUID.String(),
+						},
+					},
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeTrojan,
+				Tag:  "trojan-out",
+				TrojanOptions: option.TrojanOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: otherPort,
+					},
+					Password: userUUID.String(),
+					TLS: &option.OutboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+					},
+					DialerOptions: option.DialerOptions{
+						Detour: "vless-out",
+					},
+				},
+			},
+			{
+				Type: C.TypeVLESS,
+				Tag:  "vless-out",
+				VLESSOptions: option.VLESSOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UUID: userUUID.String(),
+					Flow: vless.FlowVision,
+					TLS: &option.OutboundTLSOptions{
+						Enabled:    true,
+						ServerName: "google.com",
+						Reality: &option.OutboundRealityOptions{
+							Enabled:   true,
+							ShortID:   "0123456789abcdef",
+							PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+						},
+						UTLS: &option.OutboundUTLSOptions{
+							Enabled: true,
+						},
+					},
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "trojan-out",
+					},
 				},
 			},
 		},

+ 204 - 0
transport/vless/client.go

@@ -0,0 +1,204 @@
+package vless
+
+import (
+	"encoding/binary"
+	"io"
+	"net"
+
+	"github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+
+	"github.com/gofrs/uuid"
+)
+
+type Client struct {
+	key  [16]byte
+	flow string
+}
+
+func NewClient(userId string, flow string) (*Client, error) {
+	user := uuid.FromStringOrNil(userId)
+	if user == uuid.Nil {
+		user = uuid.NewV5(user, userId)
+	}
+	switch flow {
+	case "", "xtls-rprx-vision":
+	default:
+		return nil, E.New("unsupported flow: " + flow)
+	}
+	return &Client{user, flow}, nil
+}
+
+func (c *Client) prepareConn(conn net.Conn) (net.Conn, error) {
+	if c.flow == FlowVision {
+		vConn, err := NewVisionConn(conn, c.key)
+		if err != nil {
+			return nil, E.Cause(err, "initialize vision")
+		}
+		conn = vConn
+	}
+	return conn, nil
+}
+
+func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (*Conn, error) {
+	vConn, err := c.prepareConn(conn)
+	if err != nil {
+		return nil, err
+	}
+	serverConn := &Conn{Conn: conn, protocolConn: vConn, key: c.key, command: vmess.CommandTCP, destination: destination, flow: c.flow}
+	return serverConn, common.Error(serverConn.Write(nil))
+}
+
+func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) (*Conn, error) {
+	vConn, err := c.prepareConn(conn)
+	if err != nil {
+		return nil, err
+	}
+	return &Conn{Conn: conn, protocolConn: vConn, key: c.key, command: vmess.CommandTCP, destination: destination, flow: c.flow}, nil
+}
+
+func (c *Client) DialPacketConn(conn net.Conn, destination M.Socksaddr) (*PacketConn, error) {
+	serverConn := &PacketConn{Conn: conn, key: c.key, destination: destination, flow: c.flow}
+	return serverConn, common.Error(serverConn.Write(nil))
+}
+
+func (c *Client) DialEarlyPacketConn(conn net.Conn, destination M.Socksaddr) (*PacketConn, error) {
+	return &PacketConn{Conn: conn, key: c.key, destination: destination, flow: c.flow}, nil
+}
+
+func (c *Client) DialXUDPPacketConn(conn net.Conn, destination M.Socksaddr) (vmess.PacketConn, error) {
+	serverConn := &Conn{Conn: conn, protocolConn: conn, key: c.key, command: vmess.CommandMux, destination: destination, flow: c.flow}
+	err := common.Error(serverConn.Write(nil))
+	if err != nil {
+		return nil, err
+	}
+	return vmess.NewXUDPConn(serverConn, destination), nil
+}
+
+func (c *Client) DialEarlyXUDPPacketConn(conn net.Conn, destination M.Socksaddr) (vmess.PacketConn, error) {
+	return vmess.NewXUDPConn(&Conn{Conn: conn, protocolConn: conn, key: c.key, command: vmess.CommandMux, destination: destination, flow: c.flow}, destination), nil
+}
+
+type Conn struct {
+	net.Conn
+	protocolConn   net.Conn
+	key            [16]byte
+	command        byte
+	destination    M.Socksaddr
+	flow           string
+	requestWritten bool
+	responseRead   bool
+}
+
+func (c *Conn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = ReadResponse(c.Conn)
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	return c.protocolConn.Read(b)
+}
+
+func (c *Conn) Write(b []byte) (n int, err error) {
+	if !c.requestWritten {
+		request := Request{c.key, c.command, c.destination, c.flow}
+		if c.protocolConn != nil {
+			err = WriteRequest(c.Conn, request, nil)
+		} else {
+			err = WriteRequest(c.Conn, request, b)
+		}
+		if err == nil {
+			n = len(b)
+		}
+		c.requestWritten = true
+		if c.protocolConn == nil {
+			return
+		}
+	}
+	return c.protocolConn.Write(b)
+}
+
+func (c *Conn) Upstream() any {
+	return c.Conn
+}
+
+type PacketConn struct {
+	net.Conn
+	key            [16]byte
+	destination    M.Socksaddr
+	flow           string
+	requestWritten bool
+	responseRead   bool
+}
+
+func (c *PacketConn) Read(b []byte) (n int, err error) {
+	if !c.responseRead {
+		err = ReadResponse(c.Conn)
+		if err != nil {
+			return
+		}
+		c.responseRead = true
+	}
+	var length uint16
+	err = binary.Read(c.Conn, binary.BigEndian, &length)
+	if err != nil {
+		return
+	}
+	if cap(b) < int(length) {
+		return 0, io.ErrShortBuffer
+	}
+	return io.ReadFull(c.Conn, b[:length])
+}
+
+func (c *PacketConn) Write(b []byte) (n int, err error) {
+	if !c.requestWritten {
+		err = WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, nil)
+		if err == nil {
+			n = len(b)
+		}
+		c.requestWritten = true
+	}
+	err = binary.Write(c.Conn, binary.BigEndian, uint16(len(b)))
+	if err != nil {
+		return
+	}
+	return c.Conn.Write(b)
+}
+
+func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	defer buffer.Release()
+	dataLen := buffer.Len()
+	binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(dataLen))
+	if !c.requestWritten {
+		err := WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, buffer.Bytes())
+		c.requestWritten = true
+		return err
+	}
+	return common.Error(c.Conn.Write(buffer.Bytes()))
+}
+
+func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	n, err = c.Read(p)
+	if err != nil {
+		return
+	}
+	addr = c.destination.UDPAddr()
+	return
+}
+
+func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	return c.Write(p)
+}
+
+func (c *PacketConn) FrontHeadroom() int {
+	return 2
+}
+
+func (c *PacketConn) Upstream() any {
+	return c.Conn
+}

+ 34 - 0
transport/vless/constant.go

@@ -0,0 +1,34 @@
+package vless
+
+import (
+	"bytes"
+
+	"github.com/sagernet/sing/common/buf"
+)
+
+var (
+	tls13SupportedVersions  = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}
+	tlsClientHandShakeStart = []byte{0x16, 0x03}
+	tlsServerHandShakeStart = []byte{0x16, 0x03, 0x03}
+	tlsApplicationDataStart = []byte{0x17, 0x03, 0x03}
+)
+
+var tls13CipherSuiteDic = map[uint16]string{
+	0x1301: "TLS_AES_128_GCM_SHA256",
+	0x1302: "TLS_AES_256_GCM_SHA384",
+	0x1303: "TLS_CHACHA20_POLY1305_SHA256",
+	0x1304: "TLS_AES_128_CCM_SHA256",
+	0x1305: "TLS_AES_128_CCM_8_SHA256",
+}
+
+func reshapeBuffer(b []byte) []*buf.Buffer {
+	const bufferLimit = 8192 - 21
+	if len(b) < bufferLimit {
+		return []*buf.Buffer{buf.As(b)}
+	}
+	index := int32(bytes.LastIndex(b, tlsApplicationDataStart))
+	if index <= 0 {
+		index = 8192 / 2
+	}
+	return []*buf.Buffer{buf.As(b[:index]), buf.As(b[index:])}
+}

+ 241 - 0
transport/vless/protocol.go

@@ -0,0 +1,241 @@
+package vless
+
+import (
+	"bytes"
+	"encoding/binary"
+	"io"
+
+	"github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/common/rw"
+)
+
+const (
+	Version    = 0
+	FlowVision = "xtls-rprx-vision"
+)
+
+type Request struct {
+	UUID        [16]byte
+	Command     byte
+	Destination M.Socksaddr
+	Flow        string
+}
+
+func ReadRequest(reader io.Reader) (*Request, error) {
+	var request Request
+
+	var version uint8
+	err := binary.Read(reader, binary.BigEndian, &version)
+	if err != nil {
+		return nil, err
+	}
+	if version != Version {
+		return nil, E.New("unknown version: ", version)
+	}
+
+	_, err = io.ReadFull(reader, request.UUID[:])
+	if err != nil {
+		return nil, err
+	}
+
+	var addonsLen uint8
+	err = binary.Read(reader, binary.BigEndian, &addonsLen)
+	if err != nil {
+		return nil, err
+	}
+
+	if addonsLen > 0 {
+		addonsBytes, err := rw.ReadBytes(reader, int(addonsLen))
+		if err != nil {
+			return nil, err
+		}
+
+		addons, err := readAddons(bytes.NewReader(addonsBytes))
+		if err != nil {
+			return nil, err
+		}
+		request.Flow = addons.Flow
+	}
+
+	err = binary.Read(reader, binary.BigEndian, &request.Command)
+	if err != nil {
+		return nil, err
+	}
+
+	if request.Command != vmess.CommandMux {
+		request.Destination, err = vmess.AddressSerializer.ReadAddrPort(reader)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &request, nil
+}
+
+type Addons struct {
+	Flow string
+	Seed string
+}
+
+func readAddons(reader io.Reader) (*Addons, error) {
+	protoHeader, err := rw.ReadByte(reader)
+	if err != nil {
+		return nil, err
+	}
+	if protoHeader != 10 {
+		return nil, E.New("unknown protobuf message header: ", protoHeader)
+	}
+
+	var addons Addons
+
+	flowLen, err := rw.ReadUVariant(reader)
+	if err != nil {
+		if err == io.EOF {
+			return &addons, nil
+		}
+		return nil, err
+	}
+	flowBytes, err := rw.ReadBytes(reader, int(flowLen))
+	if err != nil {
+		return nil, err
+	}
+	addons.Flow = string(flowBytes)
+
+	seedLen, err := rw.ReadUVariant(reader)
+	if err != nil {
+		if err == io.EOF {
+			return &addons, nil
+		}
+		return nil, err
+	}
+	seedBytes, err := rw.ReadBytes(reader, int(seedLen))
+	if err != nil {
+		return nil, err
+	}
+	addons.Seed = string(seedBytes)
+
+	return &addons, nil
+}
+
+func WriteRequest(writer io.Writer, request Request, payload []byte) error {
+	var requestLen int
+	requestLen += 1  // version
+	requestLen += 16 // uuid
+	requestLen += 1  // protobuf length
+
+	var addonsLen int
+	if request.Flow != "" {
+		addonsLen += 1 // protobuf header
+		addonsLen += UvarintLen(uint64(len(request.Flow)))
+		addonsLen += len(request.Flow)
+		requestLen += addonsLen
+	}
+	requestLen += 1 // command
+	if request.Command != vmess.CommandMux {
+		requestLen += vmess.AddressSerializer.AddrPortLen(request.Destination)
+	}
+	requestLen += len(payload)
+	_buffer := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(Version),
+		common.Error(buffer.Write(request.UUID[:])),
+		buffer.WriteByte(byte(addonsLen)),
+	)
+	if addonsLen > 0 {
+		common.Must(buffer.WriteByte(10))
+		binary.PutUvarint(buffer.Extend(UvarintLen(uint64(len(request.Flow)))), uint64(len(request.Flow)))
+		common.Must(common.Error(buffer.Write([]byte(request.Flow))))
+	}
+	common.Must(
+		buffer.WriteByte(request.Command),
+	)
+
+	if request.Command != vmess.CommandMux {
+		common.Must(vmess.AddressSerializer.WriteAddrPort(buffer, request.Destination))
+	}
+
+	common.Must1(buffer.Write(payload))
+	return common.Error(writer.Write(buffer.Bytes()))
+}
+
+func WritePacketRequest(writer io.Writer, request Request, payload []byte) error {
+	var requestLen int
+	requestLen += 1  // version
+	requestLen += 16 // uuid
+	requestLen += 1  // protobuf length
+	var addonsLen int
+	/*if request.Flow != "" {
+		addonsLen += 1 // protobuf header
+		addonsLen += UvarintLen(uint64(len(request.Flow)))
+		addonsLen += len(request.Flow)
+		requestLen += addonsLen
+	}*/
+	requestLen += 1 // command
+	requestLen += vmess.AddressSerializer.AddrPortLen(request.Destination)
+	if len(payload) > 0 {
+		requestLen += 2
+		requestLen += len(payload)
+	}
+	_buffer := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	common.Must(
+		buffer.WriteByte(Version),
+		common.Error(buffer.Write(request.UUID[:])),
+		buffer.WriteByte(byte(addonsLen)),
+	)
+
+	if addonsLen > 0 {
+		common.Must(buffer.WriteByte(10))
+		binary.PutUvarint(buffer.Extend(UvarintLen(uint64(len(request.Flow)))), uint64(len(request.Flow)))
+		common.Must(common.Error(buffer.Write([]byte(request.Flow))))
+	}
+
+	common.Must(
+		buffer.WriteByte(vmess.CommandUDP),
+		vmess.AddressSerializer.WriteAddrPort(buffer, request.Destination),
+	)
+
+	if len(payload) > 0 {
+		common.Must(
+			binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
+			common.Error(buffer.Write(payload)),
+		)
+	}
+
+	return common.Error(writer.Write(buffer.Bytes()))
+}
+
+func ReadResponse(reader io.Reader) error {
+	version, err := rw.ReadByte(reader)
+	if err != nil {
+		return err
+	}
+	if version != Version {
+		return E.New("unknown version: ", version)
+	}
+	protobufLength, err := rw.ReadByte(reader)
+	if err != nil {
+		return err
+	}
+	if protobufLength > 0 {
+		err = rw.SkipN(reader, int(protobufLength))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func UvarintLen(value uint64) int {
+	var buffer [binary.MaxVarintLen64]byte
+	return binary.PutUvarint(buffer[:], value)
+}

+ 196 - 0
transport/vless/service.go

@@ -0,0 +1,196 @@
+package vless
+
+import (
+	"context"
+	"encoding/binary"
+	"io"
+	"net"
+
+	"github.com/sagernet/sing-vmess"
+	"github.com/sagernet/sing/common/auth"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+
+	"github.com/gofrs/uuid"
+)
+
+type Service[T any] struct {
+	userMap map[[16]byte]T
+	handler Handler
+}
+
+type Handler interface {
+	N.TCPConnectionHandler
+	N.UDPConnectionHandler
+	E.Handler
+}
+
+func NewService[T any](handler Handler) *Service[T] {
+	return &Service[T]{
+		handler: handler,
+	}
+}
+
+func (s *Service[T]) UpdateUsers(userList []T, userUUIDList []string) {
+	userMap := make(map[[16]byte]T)
+	for i, userName := range userList {
+		userID := uuid.FromStringOrNil(userUUIDList[i])
+		if userID == uuid.Nil {
+			userID = uuid.NewV5(uuid.Nil, userUUIDList[i])
+		}
+		userMap[userID] = userName
+	}
+	s.userMap = userMap
+}
+
+var _ N.TCPConnectionHandler = (*Service[int])(nil)
+
+func (s *Service[T]) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
+	request, err := ReadRequest(conn)
+	if err != nil {
+		return err
+	}
+	user, loaded := s.userMap[request.UUID]
+	if !loaded {
+		return E.New("unknown UUID: ", uuid.FromBytesOrNil(request.UUID[:]))
+	}
+	ctx = auth.ContextWithUser(ctx, user)
+	metadata.Destination = request.Destination
+
+	protocolConn := conn
+	switch request.Flow {
+	case "":
+	case FlowVision:
+		protocolConn, err = NewVisionConn(conn, request.UUID)
+		if err != nil {
+			return E.Cause(err, "initialize vision")
+		}
+	}
+
+	switch request.Command {
+	case vmess.CommandTCP:
+		return s.handler.NewConnection(ctx, &serverConn{Conn: protocolConn, responseWriter: conn}, metadata)
+	case vmess.CommandUDP:
+		return s.handler.NewPacketConnection(ctx, &serverPacketConn{ExtendedConn: bufio.NewExtendedConn(conn), destination: request.Destination}, metadata)
+	case vmess.CommandMux:
+		return vmess.HandleMuxConnection(ctx, &serverConn{Conn: conn}, s.handler)
+	default:
+		return E.New("unknown command: ", request.Command)
+	}
+}
+
+type serverConn struct {
+	net.Conn
+	responseWriter  io.Writer
+	responseWritten bool
+}
+
+func (c *serverConn) Read(b []byte) (n int, err error) {
+	return c.Conn.Read(b)
+}
+
+func (c *serverConn) Write(b []byte) (n int, err error) {
+	if !c.responseWritten {
+		if c.responseWriter == nil {
+			_, err = bufio.WriteVectorised(bufio.NewVectorisedWriter(c.Conn), [][]byte{{Version, 0}, b})
+			if err == nil {
+				n = len(b)
+			}
+			c.responseWritten = true
+			return
+		} else {
+			_, err = c.responseWriter.Write([]byte{Version, 0})
+			if err != nil {
+				return
+			}
+			c.responseWritten = true
+		}
+	}
+	return c.Conn.Write(b)
+}
+
+type serverPacketConn struct {
+	N.ExtendedConn
+	responseWriter  io.Writer
+	responseWritten bool
+	destination     M.Socksaddr
+}
+
+func (c *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	n, err = c.ExtendedConn.Read(p)
+	if err != nil {
+		return
+	}
+	addr = c.destination.UDPAddr()
+	return
+}
+
+func (c *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	if !c.responseWritten {
+		if c.responseWriter == nil {
+			var packetLen [2]byte
+			binary.BigEndian.PutUint16(packetLen[:], uint16(len(p)))
+			_, err = bufio.WriteVectorised(bufio.NewVectorisedWriter(c.ExtendedConn), [][]byte{{Version, 0}, packetLen[:], p})
+			if err == nil {
+				n = len(p)
+			}
+			c.responseWritten = true
+			return
+		} else {
+			_, err = c.responseWriter.Write([]byte{Version, 0})
+			if err != nil {
+				return
+			}
+			c.responseWritten = true
+		}
+	}
+	return c.ExtendedConn.Write(p)
+}
+
+func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	var packetLen uint16
+	err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen)
+	if err != nil {
+		return
+	}
+
+	_, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen))
+	if err != nil {
+		return
+	}
+
+	destination = c.destination
+	return
+}
+
+func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	if !c.responseWritten {
+		if c.responseWriter == nil {
+			var packetLen [2]byte
+			binary.BigEndian.PutUint16(packetLen[:], uint16(buffer.Len()))
+			err := bufio.NewVectorisedWriter(c.ExtendedConn).WriteVectorised([]*buf.Buffer{buf.As([]byte{Version, 0}), buf.As(packetLen[:]), buffer})
+			c.responseWritten = true
+			return err
+		} else {
+			_, err := c.responseWriter.Write([]byte{Version, 0})
+			if err != nil {
+				return err
+			}
+			c.responseWritten = true
+		}
+	}
+	packetLen := buffer.Len()
+	binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(packetLen))
+	return c.ExtendedConn.WriteBuffer(buffer)
+}
+
+func (c *serverPacketConn) FrontHeadroom() int {
+	return 2
+}
+
+func (c *serverPacketConn) Upstream() any {
+	return c.ExtendedConn
+}

+ 309 - 0
transport/vless/vision.go

@@ -0,0 +1,309 @@
+package vless
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/tls"
+	"io"
+	"math/big"
+	"net"
+	"reflect"
+	"time"
+	"unsafe"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var tlsRegistry []func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr)
+
+func init() {
+	tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) {
+		tlsConn, loaded := conn.(*tls.Conn)
+		if !loaded {
+			return
+		}
+		return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), uintptr(unsafe.Pointer(tlsConn))
+	})
+}
+
+type VisionConn struct {
+	net.Conn
+	writer   N.VectorisedWriter
+	input    *bytes.Reader
+	rawInput *bytes.Buffer
+	netConn  net.Conn
+
+	userUUID                 [16]byte
+	isTLS                    bool
+	numberOfPacketToFilter   int
+	isTLS12orAbove           bool
+	remainingServerHello     int32
+	cipher                   uint16
+	enableXTLS               bool
+	filterTlsApplicationData bool
+	directWrite              bool
+	writeUUID                bool
+	filterUUID               bool
+	remainingContent         int
+	remainingPadding         int
+	currentCommand           int
+	directRead               bool
+	remainingReader          io.Reader
+}
+
+func NewVisionConn(conn net.Conn, userUUID [16]byte) (*VisionConn, error) {
+	var (
+		loaded         bool
+		reflectType    reflect.Type
+		reflectPointer uintptr
+		netConn        net.Conn
+	)
+	for _, tlsCreator := range tlsRegistry {
+		loaded, netConn, reflectType, reflectPointer = tlsCreator(conn)
+		if loaded {
+			break
+		}
+	}
+	if !loaded {
+		return nil, C.ErrTLSRequired
+	}
+	input, _ := reflectType.FieldByName("input")
+	rawInput, _ := reflectType.FieldByName("rawInput")
+	return &VisionConn{
+		Conn:                     conn,
+		writer:                   bufio.NewVectorisedWriter(conn),
+		input:                    (*bytes.Reader)(unsafe.Pointer(reflectPointer + input.Offset)),
+		rawInput:                 (*bytes.Buffer)(unsafe.Pointer(reflectPointer + rawInput.Offset)),
+		netConn:                  netConn,
+		userUUID:                 userUUID,
+		numberOfPacketToFilter:   8,
+		remainingServerHello:     -1,
+		filterTlsApplicationData: true,
+		writeUUID:                true,
+		filterUUID:               true,
+		remainingContent:         -1,
+		remainingPadding:         -1,
+	}, nil
+}
+
+func (c *VisionConn) Read(p []byte) (n int, err error) {
+	if c.remainingReader != nil {
+		n, err = c.remainingReader.Read(p)
+		if err == io.EOF {
+			c.remainingReader = nil
+			if n > 0 {
+				return
+			}
+		}
+	}
+	if c.directRead {
+		return c.netConn.Read(p)
+	}
+	n, err = c.Conn.Read(p)
+	if err != nil {
+		return
+	}
+	buffer := p[:n]
+	if c.filterUUID && (c.isTLS || c.numberOfPacketToFilter > 0) {
+		buffers := c.unPadding(buffer)
+		if c.remainingContent == 0 && c.remainingPadding == 0 {
+			if c.currentCommand == 1 {
+				c.filterUUID = false
+			} else if c.currentCommand == 2 {
+				c.filterUUID = false
+				c.directRead = true
+
+				inputBuffer, err := io.ReadAll(c.input)
+				if err != nil {
+					return 0, err
+				}
+				buffers = append(buffers, inputBuffer)
+
+				rawInputBuffer, err := io.ReadAll(c.rawInput)
+				if err != nil {
+					return 0, err
+				}
+
+				buffers = append(buffers, rawInputBuffer)
+			} else if c.currentCommand != 0 {
+				return 0, E.New("unknown command ", c.currentCommand)
+			}
+		}
+		if c.numberOfPacketToFilter > 0 {
+			c.filterTLS(buffers)
+		}
+		c.remainingReader = io.MultiReader(common.Map(buffers, func(it []byte) io.Reader { return bytes.NewReader(it) })...)
+		return c.remainingReader.Read(p)
+	} else {
+		if c.numberOfPacketToFilter > 0 {
+			c.filterTLS([][]byte{buffer})
+		}
+		return
+	}
+}
+
+func (c *VisionConn) Write(p []byte) (n int, err error) {
+	if c.numberOfPacketToFilter > 0 {
+		c.filterTLS([][]byte{p})
+	}
+	if c.isTLS && c.filterTlsApplicationData {
+		inputLen := len(p)
+		buffers := reshapeBuffer(p)
+		var specIndex int
+		for i, buffer := range buffers {
+			if buffer.Len() > 6 && bytes.Equal(tlsApplicationDataStart, buffer.To(3)) {
+				var command byte = 1
+				if c.enableXTLS {
+					c.directWrite = true
+					specIndex = i
+					command = 2
+				}
+				c.filterTlsApplicationData = false
+				buffers[i] = c.padding(buffer, command)
+				break
+			} else if !c.isTLS12orAbove && c.numberOfPacketToFilter == 0 {
+				c.filterTlsApplicationData = false
+				buffers[i] = c.padding(buffer, 0x01)
+				break
+			}
+			buffers[i] = c.padding(buffer, 0x00)
+		}
+		if c.directWrite {
+			encryptedBuffer := buffers[:specIndex+1]
+			err = c.writer.WriteVectorised(encryptedBuffer)
+			if err != nil {
+				return
+			}
+			buffers = buffers[specIndex+1:]
+			c.writer = bufio.NewVectorisedWriter(c.netConn)
+			time.Sleep(5 * time.Millisecond) // wtf
+		}
+		err = c.writer.WriteVectorised(buffers)
+		if err == nil {
+			n = inputLen
+		}
+		return
+	}
+	if c.directWrite {
+		return c.netConn.Write(p)
+	} else {
+		return c.Conn.Write(p)
+	}
+}
+
+func (c *VisionConn) filterTLS(buffers [][]byte) {
+	for _, buffer := range buffers {
+		c.numberOfPacketToFilter--
+		if len(buffer) > 6 {
+			if buffer[0] == 22 && buffer[1] == 3 && buffer[2] == 3 {
+				c.isTLS = true
+				if buffer[5] == 2 {
+					c.isTLS12orAbove = true
+					c.remainingServerHello = (int32(buffer[3])<<8 | int32(buffer[4])) + 5
+					if len(buffer) >= 79 && c.remainingServerHello >= 79 {
+						sessionIdLen := int32(buffer[43])
+						cipherSuite := buffer[43+sessionIdLen+1 : 43+sessionIdLen+3]
+						c.cipher = uint16(cipherSuite[0])<<8 | uint16(cipherSuite[1])
+					}
+				}
+			} else if bytes.Equal(tlsClientHandShakeStart, buffer[:2]) && buffer[5] == 1 {
+				c.isTLS = true
+			}
+		}
+		if c.remainingServerHello > 0 {
+			end := int(c.remainingServerHello)
+			if end > len(buffer) {
+				end = len(buffer)
+			}
+			c.remainingServerHello -= int32(end)
+			if bytes.Contains(buffer[:end], tls13SupportedVersions) {
+				cipher, ok := tls13CipherSuiteDic[c.cipher]
+				if ok && cipher != "TLS_AES_128_CCM_8_SHA256" {
+					c.enableXTLS = true
+				}
+				c.numberOfPacketToFilter = 0
+				return
+			} else if c.remainingServerHello == 0 {
+				c.numberOfPacketToFilter = 0
+				return
+			}
+		}
+	}
+}
+
+func (c *VisionConn) padding(buffer *buf.Buffer, command byte) *buf.Buffer {
+	contentLen := 0
+	paddingLen := 0
+	if buffer != nil {
+		contentLen = buffer.Len()
+	}
+	if contentLen < 900 {
+		l, _ := rand.Int(rand.Reader, big.NewInt(500))
+		paddingLen = int(l.Int64()) + 900 - contentLen
+	}
+	newBuffer := buf.New()
+	if c.writeUUID {
+		newBuffer.Write(c.userUUID[:])
+		c.writeUUID = false
+	}
+	newBuffer.Write([]byte{command, byte(contentLen >> 8), byte(contentLen), byte(paddingLen >> 8), byte(paddingLen)})
+	if buffer != nil {
+		newBuffer.Write(buffer.Bytes())
+		buffer.Release()
+	}
+	newBuffer.Extend(paddingLen)
+	return newBuffer
+}
+
+func (c *VisionConn) unPadding(buffer []byte) [][]byte {
+	var bufferIndex int
+	if c.remainingContent == -1 && c.remainingPadding == -1 {
+		if len(buffer) >= 21 && bytes.Equal(c.userUUID[:], buffer[:16]) {
+			bufferIndex = 16
+			c.remainingContent = 0
+			c.remainingPadding = 0
+		}
+	}
+	if c.remainingContent == -1 && c.remainingPadding == -1 {
+		return [][]byte{buffer}
+	}
+	var buffers [][]byte
+	for bufferIndex < len(buffer) {
+		if c.remainingContent <= 0 && c.remainingPadding <= 0 {
+			if c.currentCommand == 1 {
+				buffers = append(buffers, buffer[bufferIndex:])
+				break
+			} else {
+				paddingInfo := buffer[bufferIndex : bufferIndex+5]
+				c.currentCommand = int(paddingInfo[0])
+				c.remainingContent = int(paddingInfo[1])<<8 | int(paddingInfo[2])
+				c.remainingPadding = int(paddingInfo[3])<<8 | int(paddingInfo[4])
+				bufferIndex += 5
+			}
+		} else if c.remainingContent > 0 {
+			end := c.remainingContent
+			if end > len(buffer)-bufferIndex {
+				end = len(buffer) - bufferIndex
+			}
+			buffers = append(buffers, buffer[bufferIndex:bufferIndex+end])
+			c.remainingContent -= end
+			bufferIndex += end
+		} else {
+			end := c.remainingPadding
+			if end > len(buffer)-bufferIndex {
+				end = len(buffer) - bufferIndex
+			}
+			c.remainingPadding -= end
+			bufferIndex += end
+		}
+		if bufferIndex == len(buffer) {
+			break
+		}
+	}
+	return buffers
+}

+ 23 - 0
transport/vless/vision_reality.go

@@ -0,0 +1,23 @@
+//go:build with_reality_server
+
+package vless
+
+import (
+	"net"
+	"reflect"
+	"unsafe"
+
+	"github.com/sagernet/sing/common"
+
+	"github.com/nekohasekai/reality"
+)
+
+func init() {
+	tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) {
+		tlsConn, loaded := common.Cast[*reality.Conn](conn)
+		if !loaded {
+			return
+		}
+		return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), uintptr(unsafe.Pointer(tlsConn))
+	})
+}

+ 22 - 0
transport/vless/vision_utls.go

@@ -0,0 +1,22 @@
+//go:build with_utls
+
+package vless
+
+import (
+	"net"
+	"reflect"
+	"unsafe"
+
+	"github.com/sagernet/sing/common"
+	utls "github.com/sagernet/utls"
+)
+
+func init() {
+	tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) {
+		tlsConn, loaded := common.Cast[*utls.UConn](conn)
+		if !loaded {
+			return
+		}
+		return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), uintptr(unsafe.Pointer(tlsConn.Conn))
+	})
+}