Browse Source

XTLS Vision: Add `testpre` (outbound pre-connect) and `testseed` (outbound & inbound) (#5270)

https://t.me/projectXtls/1034

---------

Co-authored-by: 风扇滑翔翼 <[email protected]>
RPRX 4 days ago
parent
commit
cadcb47074

+ 6 - 2
app/proxyman/outbound/handler.go

@@ -317,8 +317,12 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
 	conn, err := internet.Dial(ctx, dest, h.streamSettings)
 	conn = h.getStatCouterConnection(conn)
 	outbounds := session.OutboundsFromContext(ctx)
-	ob := outbounds[len(outbounds)-1]
-	ob.Conn = conn
+	if outbounds != nil {
+		ob := outbounds[len(outbounds)-1]
+		ob.Conn = conn
+	} else {
+		// for Vision's pre-connect
+	}
 	return conn, err
 }
 

+ 9 - 0
infra/conf/vless.go

@@ -34,6 +34,7 @@ type VLessInboundConfig struct {
 	Decryption string                  `json:"decryption"`
 	Fallbacks  []*VLessInboundFallback `json:"fallbacks"`
 	Flow       string                  `json:"flow"`
+	Testseed   []uint32                `json:"testseed"`
 }
 
 // Build implements Buildable
@@ -73,6 +74,10 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
 			return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`)
 		}
 
+		if len(account.Testseed) < 4 {
+			account.Testseed = c.Testseed
+		}
+
 		if account.Encryption != "" {
 			return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
 		}
@@ -212,6 +217,8 @@ type VLessOutboundConfig struct {
 	Seed       string                `json:"seed"`
 	Encryption string                `json:"encryption"`
 	Reverse    *vless.Reverse        `json:"reverse"`
+	Testpre    uint32                `json:"testpre"`
+	Testseed   []uint32              `json:"testseed"`
 	Vnext      []*VLessOutboundVnext `json:"vnext"`
 }
 
@@ -258,6 +265,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
 				//account.Seed = c.Seed
 				account.Encryption = c.Encryption
 				account.Reverse = c.Reverse
+				account.Testpre = c.Testpre
+				account.Testseed = c.Testseed
 			} else {
 				if err := json.Unmarshal(rawUser, account); err != nil {
 					return nil, errors.New(`VLESS users: invalid user`).Base(err)

+ 16 - 10
proxy/proxy.go

@@ -296,11 +296,16 @@ type VisionWriter struct {
 	// internal
 	writeOnceUserUUID  []byte
 	directWriteCounter stats.Counter
+
+	testseed []uint32
 }
 
-func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, ob *session.Outbound) *VisionWriter {
+func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, ob *session.Outbound, testseed []uint32) *VisionWriter {
 	w := make([]byte, len(trafficState.UserUUID))
 	copy(w, trafficState.UserUUID)
+	if len(testseed) < 4 {
+		testseed = []uint32{900, 500, 900, 256}
+	}
 	return &VisionWriter{
 		Writer:            writer,
 		trafficState:      trafficState,
@@ -309,6 +314,7 @@ func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink boo
 		isUplink:          isUplink,
 		conn:              conn,
 		ob:                ob,
+		testseed:          testseed,
 	}
 }
 
@@ -347,7 +353,7 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
 
 	if *isPadding {
 		if len(mb) == 1 && mb[0] == nil {
-			mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx) // we do a long padding to hide vless header
+			mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx, w.testseed) // we do a long padding to hide vless header
 			return w.Writer.WriteMultiBuffer(mb)
 		}
 		isComplete := IsCompleteRecord(mb)
@@ -365,13 +371,13 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
 						command = CommandPaddingDirect
 					}
 				}
-				mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx)
+				mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx, w.testseed)
 				*isPadding = false // padding going to end
 				longPadding = false
 				continue
 			} else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early
 				*isPadding = false
-				mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx)
+				mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed)
 				break
 			}
 			var command byte = CommandPaddingContinue
@@ -381,7 +387,7 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
 					command = CommandPaddingDirect
 				}
 			}
-			mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx)
+			mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed)
 		}
 	}
 	return w.Writer.WriteMultiBuffer(mb)
@@ -488,20 +494,20 @@ func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBu
 }
 
 // XtlsPadding add padding to eliminate length signature during tls handshake
-func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context) *buf.Buffer {
+func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context, testseed []uint32) *buf.Buffer {
 	var contentLen int32 = 0
 	var paddingLen int32 = 0
 	if b != nil {
 		contentLen = b.Len()
 	}
-	if contentLen < 900 && longPadding {
-		l, err := rand.Int(rand.Reader, big.NewInt(500))
+	if contentLen < int32(testseed[0]) && longPadding {
+		l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[1])))
 		if err != nil {
 			errors.LogDebugInner(ctx, err, "failed to generate padding")
 		}
-		paddingLen = int32(l.Int64()) + 900 - contentLen
+		paddingLen = int32(l.Int64()) + int32(testseed[2]) - contentLen
 	} else {
-		l, err := rand.Int(rand.Reader, big.NewInt(256))
+		l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[3])))
 		if err != nil {
 			errors.LogDebugInner(ctx, err, "failed to generate padding")
 		}

+ 7 - 0
proxy/vless/account.go

@@ -22,6 +22,8 @@ func (a *Account) AsAccount() (protocol.Account, error) {
 		Seconds:    a.Seconds,
 		Padding:    a.Padding,
 		Reverse:    a.Reverse,
+		Testpre:    a.Testpre,
+		Testseed:   a.Testseed,
 	}, nil
 }
 
@@ -38,6 +40,9 @@ type MemoryAccount struct {
 	Padding    string
 
 	Reverse *Reverse
+
+	Testpre  uint32
+	Testseed []uint32
 }
 
 // Equals implements protocol.Account.Equals().
@@ -58,5 +63,7 @@ func (a *MemoryAccount) ToProto() proto.Message {
 		Seconds:    a.Seconds,
 		Padding:    a.Padding,
 		Reverse:    a.Reverse,
+		Testpre:    a.Testpre,
+		Testseed:   a.Testseed,
 	}
 }

+ 27 - 8
proxy/vless/account.pb.go

@@ -79,6 +79,8 @@ type Account struct {
 	Seconds    uint32   `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"`
 	Padding    string   `protobuf:"bytes,6,opt,name=padding,proto3" json:"padding,omitempty"`
 	Reverse    *Reverse `protobuf:"bytes,7,opt,name=reverse,proto3" json:"reverse,omitempty"`
+	Testpre    uint32   `protobuf:"varint,8,opt,name=testpre,proto3" json:"testpre,omitempty"`
+	Testseed   []uint32 `protobuf:"varint,9,rep,packed,name=testseed,proto3" json:"testseed,omitempty"`
 }
 
 func (x *Account) Reset() {
@@ -160,6 +162,20 @@ func (x *Account) GetReverse() *Reverse {
 	return nil
 }
 
+func (x *Account) GetTestpre() uint32 {
+	if x != nil {
+		return x.Testpre
+	}
+	return 0
+}
+
+func (x *Account) GetTestseed() []uint32 {
+	if x != nil {
+		return x.Testseed
+	}
+	return nil
+}
+
 var File_proxy_vless_account_proto protoreflect.FileDescriptor
 
 var file_proxy_vless_account_proto_rawDesc = []byte{
@@ -167,7 +183,7 @@ var file_proxy_vless_account_proto_rawDesc = []byte{
 	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, 0x1b, 0x0a,
 	0x07, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0xd0, 0x01, 0x0a, 0x07, 0x41,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x86, 0x02, 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, 0x0a, 0x65, 0x6e,
@@ -180,13 +196,16 @@ var file_proxy_vless_account_proto_rawDesc = []byte{
 	0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x33, 0x0a, 0x07, 0x72, 0x65, 0x76, 0x65,
 	0x72, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79,
 	0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x76,
-	0x65, 0x72, 0x73, 0x65, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 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,
+	0x65, 0x72, 0x73, 0x65, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x12, 0x18, 0x0a,
+	0x07, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07,
+	0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x73, 0x74, 0x73,
+	0x65, 0x65, 0x64, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x08, 0x74, 0x65, 0x73, 0x74, 0x73,
+	0x65, 0x65, 0x64, 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 (

+ 3 - 0
proxy/vless/account.proto

@@ -22,4 +22,7 @@ message Account {
   string padding = 6;
 
   Reverse reverse = 7;
+
+  uint32 testpre = 8;
+  repeated uint32 testseed = 9;
 }

+ 1 - 1
proxy/vless/encoding/addons.go

@@ -68,7 +68,7 @@ func EncodeBodyAddons(writer buf.Writer, request *protocol.RequestHeader, reques
 		return NewMultiLengthPacketWriter(writer)
 	}
 	if requestAddons.Flow == vless.XRV {
-		return proxy.NewVisionWriter(writer, state, isUplink, context, conn, ob)
+		return proxy.NewVisionWriter(writer, state, isUplink, context, conn, ob, request.User.Account.(*vless.MemoryAccount).Testseed)
 	}
 	return writer
 }

+ 47 - 8
proxy/vless/outbound/outbound.go

@@ -7,6 +7,7 @@ import (
 	"encoding/base64"
 	"reflect"
 	"strings"
+	"sync"
 	"time"
 	"unsafe"
 
@@ -15,6 +16,7 @@ import (
 	"github.com/xtls/xray-core/app/reverse"
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/buf"
+	xctx "github.com/xtls/xray-core/common/ctx"
 	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/mux"
 	"github.com/xtls/xray-core/common/net"
@@ -52,6 +54,10 @@ type Handler struct {
 	cone          bool
 	encryption    *encryption.ClientInstance
 	reverse       *Reverse
+
+	testpre  uint32
+	initpre  sync.Once
+	preConns chan stat.Connection
 }
 
 // New creates a new VLess outbound handler.
@@ -105,11 +111,16 @@ func New(ctx context.Context, config *Config) (*Handler, error) {
 		}()
 	}
 
+	handler.testpre = a.Testpre
+
 	return handler, nil
 }
 
 // Close implements common.Closable.Close().
 func (h *Handler) Close() error {
+	if h.preConns != nil {
+		close(h.preConns)
+	}
 	if h.reverse != nil {
 		return h.reverse.Close()
 	}
@@ -128,18 +139,46 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
 	rec := h.server
 	var conn stat.Connection
 
-	if err := retry.ExponentialBackoff(5, 200).On(func() error {
-		var err error
-		conn, err = dialer.Dial(ctx, rec.Destination)
-		if err != nil {
-			return err
+	if h.testpre > 0 && h.reverse == nil {
+		h.initpre.Do(func() {
+			h.preConns = make(chan stat.Connection)
+			for range h.testpre { // TODO: randomize
+				go func() {
+					defer func() { recover() }()
+					ctx := xctx.ContextWithID(context.Background(), session.NewID())
+					for {
+						time.Sleep(time.Millisecond * 200) // TODO: randomize
+						conn, err := dialer.Dial(ctx, rec.Destination)
+						if err != nil {
+							errors.LogWarningInner(ctx, err, "pre-connect failed")
+							continue
+						}
+						h.preConns <- conn
+					}
+				}()
+			}
+		})
+		if conn = <-h.preConns; conn == nil {
+			return errors.New("closed handler").AtWarning()
+		}
+	}
+
+	if conn == nil {
+		if err := retry.ExponentialBackoff(5, 200).On(func() error {
+			var err error
+			conn, err = dialer.Dial(ctx, rec.Destination)
+			if err != nil {
+				return err
+			}
+			return nil
+		}); err != nil {
+			return errors.New("failed to find an available destination").Base(err).AtWarning()
 		}
-		return nil
-	}); err != nil {
-		return errors.New("failed to find an available destination").Base(err).AtWarning()
 	}
 	defer conn.Close()
 
+	ob.Conn = conn // for Vision's pre-connect
+
 	iConn := conn
 	if statConn, ok := iConn.(*stat.CounterConnection); ok {
 		iConn = statConn.Connection