Browse Source

VLESS Encryption: Add customizable 1-RTT padding parameters; Decrease memory using; Chores

Completes https://github.com/XTLS/Xray-core/pull/5067

---------

Co-authored-by: wwqgtxx <[email protected]>
RPRX 4 months ago
parent
commit
e8b02cd664

+ 3 - 0
common/crypto/crypto.go

@@ -10,6 +10,9 @@ func RandBetween(from int64, to int64) int64 {
 	if from == to {
 		return from
 	}
+	if from > to {
+		from, to = to, from
+	}
 	bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
 	return from + bigInt.Int64()
 }

+ 27 - 5
infra/conf/vless.go

@@ -74,7 +74,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
 		}
 
 		if account.Encryption != "" {
-			return nil, errors.New(`VLESS clients: "encryption" should not in inbound settings`)
+			return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
 		}
 
 		user.Account = serial.ToTypedMessage(account)
@@ -107,12 +107,21 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
 			}
 			config.Seconds = uint32(i)
 		}
-		for i := 3; i < len(s); i++ {
-			if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 64 {
+		padding := 0
+		for _, r := range s[3:] {
+			if len(r) < 20 {
+				padding += len(r) + 1
+				continue
+			}
+			if b, _ := base64.RawURLEncoding.DecodeString(r); len(b) != 32 && len(b) != 64 {
 				return false
 			}
 		}
 		config.Decryption = config.Decryption[27+len(s[2]):]
+		if padding > 0 {
+			config.Padding = config.Decryption[:padding-1]
+			config.Decryption = config.Decryption[padding:]
+		}
 		return true
 	}() && config.Decryption != "none" {
 		if config.Decryption == "" {
@@ -121,6 +130,10 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
 		return nil, errors.New(`VLESS settings: unsupported "decryption": ` + config.Decryption)
 	}
 
+	if config.Decryption != "none" && c.Fallbacks != nil {
+		return nil, errors.New(`VLESS settings: "fallbacks" can not be used together with "decryption"`)
+	}
+
 	for _, fb := range c.Fallbacks {
 		var i uint16
 		var s string
@@ -250,12 +263,21 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
 				default:
 					return false
 				}
-				for i := 3; i < len(s); i++ {
-					if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 1184 {
+				padding := 0
+				for _, r := range s[3:] {
+					if len(r) < 20 {
+						padding += len(r) + 1
+						continue
+					}
+					if b, _ := base64.RawURLEncoding.DecodeString(r); len(b) != 32 && len(b) != 1184 {
 						return false
 					}
 				}
 				account.Encryption = account.Encryption[27+len(s[2]):]
+				if padding > 0 {
+					account.Padding = account.Encryption[:padding-1]
+					account.Encryption = account.Encryption[padding:]
+				}
 				return true
 			}() && account.Encryption != "none" {
 				if account.Encryption == "" {

+ 3 - 0
proxy/vless/account.go

@@ -20,6 +20,7 @@ func (a *Account) AsAccount() (protocol.Account, error) {
 		Encryption: a.Encryption, // needs parser here?
 		XorMode:    a.XorMode,
 		Seconds:    a.Seconds,
+		Padding:    a.Padding,
 	}, nil
 }
 
@@ -33,6 +34,7 @@ type MemoryAccount struct {
 	Encryption string
 	XorMode    uint32
 	Seconds    uint32
+	Padding    string
 }
 
 // Equals implements protocol.Account.Equals().
@@ -51,5 +53,6 @@ func (a *MemoryAccount) ToProto() proto.Message {
 		Encryption: a.Encryption,
 		XorMode:    a.XorMode,
 		Seconds:    a.Seconds,
+		Padding:    a.Padding,
 	}
 }

+ 17 - 7
proxy/vless/account.pb.go

@@ -32,6 +32,7 @@ type Account struct {
 	Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"`
 	XorMode    uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"`
 	Seconds    uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"`
+	Padding    string `protobuf:"bytes,6,opt,name=padding,proto3" json:"padding,omitempty"`
 }
 
 func (x *Account) Reset() {
@@ -99,12 +100,19 @@ func (x *Account) GetSeconds() uint32 {
 	return 0
 }
 
+func (x *Account) GetPadding() string {
+	if x != nil {
+		return x.Padding
+	}
+	return ""
+}
+
 var File_proxy_vless_account_proto protoreflect.FileDescriptor
 
 var file_proxy_vless_account_proto_rawDesc = []byte{
 	0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63,
 	0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x81, 0x01,
+	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x9b, 0x01,
 	0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
 	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f,
 	0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a,
@@ -113,12 +121,14 @@ var file_proxy_vless_account_proto_rawDesc = []byte{
 	0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07,
 	0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x6f, 0x6e,
 	0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64,
-	0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72,
-	0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 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, 0x76, 0x6c, 0x65,
-	0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e,
-	0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x52, 0x0a, 0x14, 0x63,
+	0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c,
+	0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 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, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58,
+	0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62,
+	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (

+ 1 - 0
proxy/vless/account.proto

@@ -15,4 +15,5 @@ message Account {
   string encryption = 3;
   uint32 xorMode = 4;
   uint32 seconds = 5;
+  string padding = 6;
 }

+ 11 - 17
proxy/vless/encoding/encoding.go

@@ -172,7 +172,7 @@ func DecodeResponseHeader(reader io.Reader, request *protocol.RequestHeader) (*A
 }
 
 // XtlsRead filter and read xtls protocol
-func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, conn net.Conn, peerCache *[]byte, input *bytes.Reader, rawInput *bytes.Buffer, trafficState *proxy.TrafficState, ob *session.Outbound, isUplink bool, ctx context.Context) error {
+func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, trafficState *proxy.TrafficState, ob *session.Outbound, isUplink bool, ctx context.Context) error {
 	err := func() error {
 		for {
 			if isUplink && trafficState.Inbound.UplinkReaderDirectCopy || !isUplink && trafficState.Outbound.DownlinkReaderDirectCopy {
@@ -194,23 +194,17 @@ func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer,
 			if !buffer.IsEmpty() {
 				timer.Update()
 				if isUplink && trafficState.Inbound.UplinkReaderDirectCopy || !isUplink && trafficState.Outbound.DownlinkReaderDirectCopy {
-					// XTLS Vision processes struct Encryption Conn's peerCache or TLS Conn's input and rawInput
-					if peerCache != nil {
-						if len(*peerCache) != 0 {
-							buffer = buf.MergeBytes(buffer, *peerCache)
-						}
-					} else {
-						if inputBuffer, err := buf.ReadFrom(input); err == nil {
-							if !inputBuffer.IsEmpty() {
-								buffer, _ = buf.MergeMulti(buffer, inputBuffer)
-							}
-						}
-						if rawInputBuffer, err := buf.ReadFrom(rawInput); err == nil {
-							if !rawInputBuffer.IsEmpty() {
-								buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
-							}
-						}
+					// XTLS Vision processes TLS-like conn's input and rawInput
+					if inputBuffer, err := buf.ReadFrom(input); err == nil && !inputBuffer.IsEmpty() {
+						buffer, _ = buf.MergeMulti(buffer, inputBuffer)
 					}
+					if rawInputBuffer, err := buf.ReadFrom(rawInput); err == nil && !rawInputBuffer.IsEmpty() {
+						buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
+					}
+					*input = bytes.Reader{} // release memory
+					input = nil
+					*rawInput = bytes.Buffer{} // release memory
+					rawInput = nil
 				}
 				if werr := writer.WriteMultiBuffer(buffer); werr != nil {
 					return werr

+ 18 - 11
proxy/vless/encryption/client.go

@@ -10,7 +10,6 @@ import (
 	"sync"
 	"time"
 
-	"github.com/xtls/xray-core/common/crypto"
 	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/protocol"
 	"lukechampine.com/blake3"
@@ -23,6 +22,8 @@ type ClientInstance struct {
 	RelaysLength  int
 	XorMode       uint32
 	Seconds       uint32
+	PaddingLens   [][2]int
+	PaddingGaps   [][2]int
 
 	RWLock sync.RWMutex
 	Expire time.Time
@@ -30,15 +31,13 @@ type ClientInstance struct {
 	Ticket []byte
 }
 
-func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
+func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) {
 	if i.NfsPKeys != nil {
-		err = errors.New("already initialized")
-		return
+		return errors.New("already initialized")
 	}
 	l := len(nfsPKeysBytes)
 	if l == 0 {
-		err = errors.New("empty nfsPKeysBytes")
-		return
+		return errors.New("empty nfsPKeysBytes")
 	}
 	i.NfsPKeys = make([]any, l)
 	i.NfsPKeysBytes = nfsPKeysBytes
@@ -60,7 +59,7 @@ func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (
 	i.RelaysLength -= 32
 	i.XorMode = xorMode
 	i.Seconds = seconds
-	return
+	return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
 }
 
 func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
@@ -71,7 +70,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 
 	ivAndRealysLength := 16 + i.RelaysLength
 	pfsKeyExchangeLength := 18 + 1184 + 32 + 16
-	paddingLength := int(crypto.RandBetween(100, 1000))
+	paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps)
 	clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength)
 
 	iv := clientHello[:16]
@@ -140,10 +139,18 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 	nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
 	nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
 
-	if _, err := conn.Write(clientHello); err != nil {
-		return nil, err
+	paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0]
+	for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
+		if l > 0 {
+			if _, err := conn.Write(clientHello[:l]); err != nil {
+				return nil, err
+			}
+			clientHello = clientHello[l:]
+		}
+		if len(paddingGaps) > i {
+			time.Sleep(paddingGaps[i])
+		}
 	}
-	// padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
 
 	encryptedPfsPublicKey := make([]byte, 1088+32+16)
 	if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {

+ 67 - 17
proxy/vless/encryption/common.go

@@ -7,10 +7,12 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/xtls/xray-core/common/crypto"
 	"github.com/xtls/xray-core/common/errors"
 	"golang.org/x/crypto/chacha20poly1305"
 	"lukechampine.com/blake3"
@@ -31,15 +33,14 @@ type CommonConn struct {
 	AEAD        *AEAD
 	PeerAEAD    *AEAD
 	PeerPadding []byte
-	PeerInBytes []byte
-	PeerCache   []byte
+	rawInput    bytes.Buffer
+	input       bytes.Reader
 }
 
 func NewCommonConn(conn net.Conn, useAES bool) *CommonConn {
 	return &CommonConn{
-		Conn:        conn,
-		UseAES:      useAES,
-		PeerInBytes: make([]byte, 5+17000), // no need to use sync.Pool, because we are always reading
+		Conn:   conn,
+		UseAES: useAES,
 	}
 }
 
@@ -99,16 +100,14 @@ func (c *CommonConn) Read(b []byte) (int, error) {
 		}
 		c.PeerPadding = nil
 	}
-	if len(c.PeerCache) > 0 {
-		n := copy(b, c.PeerCache)
-		c.PeerCache = c.PeerCache[n:]
-		return n, nil
+	if c.input.Len() > 0 {
+		return c.input.Read(b)
 	}
-	peerHeader := c.PeerInBytes[:5]
-	if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
+	peerHeader := [5]byte{}
+	if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil {
 		return 0, err
 	}
-	l, err := DecodeHeader(c.PeerInBytes[:5]) // l: 17~17000
+	l, err := DecodeHeader(peerHeader[:]) // l: 17~17000
 	if err != nil {
 		if c.Client != nil && strings.Contains(err.Error(), "invalid header: ") { // client's 0-RTT
 			c.Client.RWLock.Lock()
@@ -121,7 +120,10 @@ func (c *CommonConn) Read(b []byte) (int, error) {
 		return 0, err
 	}
 	c.Client = nil
-	peerData := c.PeerInBytes[5 : 5+l]
+	if c.rawInput.Cap() < l {
+		c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading
+	}
+	peerData := c.rawInput.Bytes()[:l]
 	if _, err := io.ReadFull(c.Conn, peerData); err != nil {
 		return 0, err
 	}
@@ -131,9 +133,9 @@ func (c *CommonConn) Read(b []byte) (int, error) {
 	}
 	var newAEAD *AEAD
 	if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) {
-		newAEAD = NewAEAD(c.PeerInBytes[:5+l], c.UnitedKey, c.UseAES)
+		newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES)
 	}
-	_, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader)
+	_, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:])
 	if newAEAD != nil {
 		c.PeerAEAD = newAEAD
 	}
@@ -141,7 +143,7 @@ func (c *CommonConn) Read(b []byte) (int, error) {
 		return 0, err
 	}
 	if len(dst) > len(b) {
-		c.PeerCache = dst[copy(b, dst):]
+		c.input.Reset(dst[copy(b, dst):])
 		dst = b // for len(dst)
 	}
 	return len(dst), nil
@@ -213,7 +215,55 @@ func DecodeHeader(h []byte) (l int, err error) {
 		l = 0
 	}
 	if l < 17 || l > 17000 { // TODO: TLSv1.3 max length
-		err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) // DO NOT CHANGE: relied by client's Read()
+		err = errors.New("invalid header: " + fmt.Sprintf("%v", h[:5])) // DO NOT CHANGE: relied by client's Read()
+	}
+	return
+}
+
+func ParsePadding(padding string, paddingLens, paddingGaps *[][2]int) (err error) {
+	if padding == "" {
+		return
+	}
+	maxLen := 0
+	for i, s := range strings.Split(padding, ".") {
+		x := strings.SplitN(s, "-", 2)
+		if len(x) != 2 || x[0] == "" || x[1] == "" {
+			return errors.New("invalid padding lenth/gap parameter: " + s)
+		}
+		y := [2]int{}
+		if y[0], err = strconv.Atoi(x[0]); err != nil {
+			return
+		}
+		if y[1], err = strconv.Atoi(x[1]); err != nil {
+			return
+		}
+		if i == 0 && (y[0] < 17 || y[1] < 17) {
+			return errors.New("first padding length must be larger than 16")
+		}
+		if i%2 == 0 {
+			*paddingLens = append(*paddingLens, y)
+			maxLen += max(y[0], y[1])
+		} else {
+			*paddingGaps = append(*paddingGaps, y)
+		}
+	}
+	if maxLen > 65535 {
+		return errors.New("total padding length must be smaller than 65536")
+	}
+	return
+}
+
+func CreatPadding(paddingLens, paddingGaps [][2]int) (length int, lens []int, gaps []time.Duration) {
+	if len(paddingLens) == 0 {
+		paddingLens = [][2]int{{111, 1111}, {3333, -1234}}
+		paddingGaps = [][2]int{{111, -66}}
+	}
+	for _, l := range paddingLens {
+		lens = append(lens, int(max(0, crypto.RandBetween(int64(l[0]), int64(l[1])))))
+		length += lens[len(lens)-1]
+	}
+	for _, g := range paddingGaps {
+		gaps = append(gaps, time.Duration(max(0, crypto.RandBetween(int64(g[0]), int64(g[1]))))*time.Millisecond)
 	}
 	return
 }

+ 37 - 20
proxy/vless/encryption/server.go

@@ -30,21 +30,21 @@ type ServerInstance struct {
 	RelaysLength  int
 	XorMode       uint32
 	Seconds       uint32
+	PaddingLens   [][2]int
+	PaddingGaps   [][2]int
 
 	RWLock   sync.RWMutex
 	Sessions map[[16]byte]*ServerSession
 	Closed   bool
 }
 
-func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
+func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) {
 	if i.NfsSKeys != nil {
-		err = errors.New("already initialized")
-		return
+		return errors.New("already initialized")
 	}
 	l := len(nfsSKeysBytes)
 	if l == 0 {
-		err = errors.New("empty nfsSKeysBytes")
-		return
+		return errors.New("empty nfsSKeysBytes")
 	}
 	i.NfsSKeys = make([]any, l)
 	i.NfsPKeysBytes = make([][]byte, l)
@@ -88,7 +88,7 @@ func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (
 			}
 		}()
 	}
-	return
+	return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps)
 }
 
 func (i *ServerInstance) Close() (err error) {
@@ -98,7 +98,7 @@ func (i *ServerInstance) Close() (err error) {
 	return
 }
 
-func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
+func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) {
 	if i.NfsSKeys == nil {
 		return nil, errors.New("uninitialized")
 	}
@@ -108,6 +108,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 	if _, err := io.ReadFull(conn, ivAndRelays); err != nil {
 		return nil, err
 	}
+	if fallback != nil {
+		*fallback = append(*fallback, ivAndRelays...)
+	}
 	iv := ivAndRelays[:16]
 	relays := ivAndRelays[16:]
 	var nfsKey []byte
@@ -157,6 +160,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 	if _, err := io.ReadFull(conn, encryptedLength); err != nil {
 		return nil, err
 	}
+	if fallback != nil {
+		*fallback = append(*fallback, encryptedLength...)
+	}
 	decryptedLength := make([]byte, 2)
 	if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil {
 		c.UseAES = !c.UseAES
@@ -165,6 +171,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 			return nil, err
 		}
 	}
+	if fallback != nil {
+		*fallback = nil
+	}
 	length := DecodeLength(decryptedLength)
 
 	if length == 32 {
@@ -183,7 +192,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 		s := i.Sessions[[16]byte(ticket)]
 		i.RWLock.RUnlock()
 		if s == nil {
-			noises := make([]byte, crypto.RandBetween(1268, 2268)) // matches 1-RTT's server hello length for "random", though it is not important, just for example
+			noises := make([]byte, crypto.RandBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example
 			var err error
 			for err == nil {
 				rand.Read(noises)
@@ -237,13 +246,22 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 	c.UnitedKey = append(pfsKey, nfsKey...)
 	c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES)
 	c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES)
+
 	ticket := make([]byte, 16)
 	rand.Read(ticket)
 	copy(ticket, EncodeLength(int(i.Seconds*4/5)))
+	if i.Seconds > 0 {
+		i.RWLock.Lock()
+		i.Sessions[[16]byte(ticket)] = &ServerSession{
+			Expire: time.Now().Add(time.Duration(i.Seconds) * time.Second),
+			PfsKey: pfsKey,
+		}
+		i.RWLock.Unlock()
+	}
 
 	pfsKeyExchangeLength := 1088 + 32 + 16
 	encryptedTicketLength := 32
-	paddingLength := int(crypto.RandBetween(100, 1000))
+	paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps)
 	serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength)
 	nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil)
 	c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket, nil)
@@ -251,18 +269,17 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
 	c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
 	c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
 
-	if _, err := conn.Write(serverHello); err != nil {
-		return nil, err
-	}
-	// padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
-
-	if i.Seconds > 0 {
-		i.RWLock.Lock()
-		i.Sessions[[16]byte(ticket)] = &ServerSession{
-			Expire: time.Now().Add(time.Duration(i.Seconds) * time.Second),
-			PfsKey: pfsKey,
+	paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0]
+	for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control
+		if l > 0 {
+			if _, err := conn.Write(serverHello[:l]); err != nil {
+				return nil, err
+			}
+			serverHello = serverHello[l:]
+		}
+		if len(paddingGaps) > i {
+			time.Sleep(paddingGaps[i])
 		}
-		i.RWLock.Unlock()
 	}
 
 	// important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern

+ 19 - 9
proxy/vless/inbound/config.pb.go

@@ -116,6 +116,7 @@ type Config struct {
 	Decryption string           `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"`
 	XorMode    uint32           `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"`
 	Seconds    uint32           `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"`
+	Padding    string           `protobuf:"bytes,6,opt,name=padding,proto3" json:"padding,omitempty"`
 }
 
 func (x *Config) Reset() {
@@ -183,6 +184,13 @@ func (x *Config) GetSeconds() uint32 {
 	return 0
 }
 
+func (x *Config) GetPadding() string {
+	if x != nil {
+		return x.Padding
+	}
+	return ""
+}
+
 var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor
 
 var file_proxy_vless_inbound_config_proto_rawDesc = []byte{
@@ -199,7 +207,7 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{
 	0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
 	0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20,
 	0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65,
-	0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xd4, 0x01,
+	0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xee, 0x01,
 	0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65,
 	0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79,
 	0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
@@ -213,14 +221,16 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{
 	0x12, 0x18, 0x0a, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
 	0x0d, 0x52, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65,
 	0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x65, 0x63,
-	0x6f, 0x6e, 0x64, 0x73, 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79,
-	0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62,
-	0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, 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, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e,
-	0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f,
-	0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64,
-	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6f, 0x6e, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x6a,
+	0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79,
+	0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01,
+	0x5a, 0x2d, 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, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa,
+	0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65,
+	0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (

+ 1 - 0
proxy/vless/inbound/config.proto

@@ -24,4 +24,5 @@ message Config {
   string decryption = 3;
   uint32 xorMode = 4;
   uint32 seconds = 5;
+  string padding = 6;
 }

+ 10 - 13
proxy/vless/inbound/inbound.go

@@ -92,7 +92,7 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val
 			nfsSKeysBytes = append(nfsSKeysBytes, b)
 		}
 		handler.decryption = &encryption.ServerInstance{}
-		if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.Seconds); err != nil {
+		if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.Seconds, config.Padding); err != nil {
 			return nil, errors.New("failed to use decryption").Base(err).AtError()
 		}
 	}
@@ -220,8 +220,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
 
 	if h.decryption != nil {
 		var err error
-		connection, err = h.decryption.Handshake(connection)
-		if err != nil {
+		if connection, err = h.decryption.Handshake(connection, nil); err != nil {
 			return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo()
 		}
 	}
@@ -491,7 +490,6 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
 		// Flow: requestAddons.Flow,
 	}
 
-	var peerCache *[]byte
 	var input *bytes.Reader
 	var rawInput *bytes.Buffer
 	switch requestAddons.Flow {
@@ -504,16 +502,15 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
 			case protocol.RequestCommandMux:
 				fallthrough // we will break Mux connections that contain TCP requests
 			case protocol.RequestCommandTCP:
-				if serverConn, ok := connection.(*encryption.CommonConn); ok {
-					peerCache = &serverConn.PeerCache
-					if _, ok := serverConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) {
-						inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice
-					}
-					break
-				}
 				var t reflect.Type
 				var p uintptr
-				if tlsConn, ok := iConn.(*tls.Conn); ok {
+				if commonConn, ok := connection.(*encryption.CommonConn); ok {
+					if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) {
+						inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice
+					}
+					t = reflect.TypeOf(commonConn).Elem()
+					p = uintptr(unsafe.Pointer(commonConn))
+				} else if tlsConn, ok := iConn.(*tls.Conn); ok {
 					if tlsConn.ConnectionState().Version != gotls.VersionTLS13 {
 						return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version).AtWarning()
 					}
@@ -579,7 +576,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
 		if requestAddons.Flow == vless.XRV {
 			ctx1 := session.ContextWithInbound(ctx, nil) // TODO enable splice
 			clientReader = proxy.NewVisionReader(clientReader, trafficState, true, ctx1)
-			err = encoding.XtlsRead(clientReader, serverWriter, timer, connection, peerCache, input, rawInput, trafficState, nil, true, ctx1)
+			err = encoding.XtlsRead(clientReader, serverWriter, timer, connection, input, rawInput, trafficState, nil, true, ctx1)
 		} else {
 			// from clientReader.ReadMultiBuffer to serverWriter.WriteMultiBuffer
 			err = buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer))

+ 10 - 13
proxy/vless/outbound/outbound.go

@@ -77,7 +77,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) {
 			nfsPKeysBytes = append(nfsPKeysBytes, b)
 		}
 		handler.encryption = &encryption.ClientInstance{}
-		if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds); err != nil {
+		if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds, a.Padding); err != nil {
 			return nil, errors.New("failed to use encryption").Base(err).AtError()
 		}
 	}
@@ -118,8 +118,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 
 	if h.encryption != nil {
 		var err error
-		conn, err = h.encryption.Handshake(conn)
-		if err != nil {
+		if conn, err = h.encryption.Handshake(conn); err != nil {
 			return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo()
 		}
 	}
@@ -146,7 +145,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 		Flow: account.Flow,
 	}
 
-	var peerCache *[]byte
 	var input *bytes.Reader
 	var rawInput *bytes.Buffer
 	allowUDP443 := false
@@ -165,16 +163,15 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 		case protocol.RequestCommandMux:
 			fallthrough // let server break Mux connections that contain TCP requests
 		case protocol.RequestCommandTCP:
-			if clientConn, ok := conn.(*encryption.CommonConn); ok {
-				peerCache = &clientConn.PeerCache
-				if _, ok := clientConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) {
-					ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice
-				}
-				break
-			}
 			var t reflect.Type
 			var p uintptr
-			if tlsConn, ok := iConn.(*tls.Conn); ok {
+			if commonConn, ok := conn.(*encryption.CommonConn); ok {
+				if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) {
+					ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice
+				}
+				t = reflect.TypeOf(commonConn).Elem()
+				p = uintptr(unsafe.Pointer(commonConn))
+			} else if tlsConn, ok := iConn.(*tls.Conn); ok {
 				t = reflect.TypeOf(tlsConn.Conn).Elem()
 				p = uintptr(unsafe.Pointer(tlsConn.Conn))
 			} else if utlsConn, ok := iConn.(*tls.UConn); ok {
@@ -306,7 +303,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 		}
 
 		if requestAddons.Flow == vless.XRV {
-			err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, peerCache, input, rawInput, trafficState, ob, false, ctx)
+			err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, input, rawInput, trafficState, ob, false, ctx)
 		} else {
 			// from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBuffer
 			err = buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer))