From 2dc972986404a9fef62d81402a1a0d6f4f413230 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 2 Nov 2025 02:44:44 +0000 Subject: [PATCH] VLESS outbound: Add pre-connect (early test, for Vision Seed) https://t.me/projectXtls/1034 --- infra/conf/vless.go | 2 ++ proxy/vless/account.go | 4 +++ proxy/vless/account.pb.go | 26 +++++++++++----- proxy/vless/account.proto | 2 ++ proxy/vless/outbound/outbound.go | 52 +++++++++++++++++++++++++++----- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index efb480a0..2e573305 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -212,6 +212,7 @@ type VLessOutboundConfig struct { Seed string `json:"seed"` Encryption string `json:"encryption"` Reverse *vless.Reverse `json:"reverse"` + Testpre uint32 `json:"testpre"` Vnext []*VLessOutboundVnext `json:"vnext"` } @@ -258,6 +259,7 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { //account.Seed = c.Seed account.Encryption = c.Encryption account.Reverse = c.Reverse + account.Testpre = c.Testpre } else { if err := json.Unmarshal(rawUser, account); err != nil { return nil, errors.New(`VLESS users: invalid user`).Base(err) diff --git a/proxy/vless/account.go b/proxy/vless/account.go index ac00ea53..ead3c7c2 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -22,6 +22,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { Seconds: a.Seconds, Padding: a.Padding, Reverse: a.Reverse, + Testpre: a.Testpre, }, nil } @@ -38,6 +39,8 @@ type MemoryAccount struct { Padding string Reverse *Reverse + + Testpre uint32 } // Equals implements protocol.Account.Equals(). @@ -58,5 +61,6 @@ func (a *MemoryAccount) ToProto() proto.Message { Seconds: a.Seconds, Padding: a.Padding, Reverse: a.Reverse, + Testpre: a.Testpre, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index 5822f512..2c59f0d0 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -79,6 +79,7 @@ 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"` } func (x *Account) Reset() { @@ -160,6 +161,13 @@ func (x *Account) GetReverse() *Reverse { return nil } +func (x *Account) GetTestpre() uint32 { + if x != nil { + return x.Testpre + } + return 0 +} + var File_proxy_vless_account_proto protoreflect.FileDescriptor var file_proxy_vless_account_proto_rawDesc = []byte{ @@ -167,7 +175,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, 0xea, 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, 0x0a, 0x65, 0x6e, @@ -180,13 +188,15 @@ 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, 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 ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 047311dd..74d0e3dc 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -22,4 +22,6 @@ message Account { string padding = 6; Reverse reverse = 7; + + uint32 testpre = 8; } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index c425151f..add9ece0 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "reflect" "strings" + "sync" "time" "unsafe" @@ -52,6 +53,10 @@ type Handler struct { cone bool encryption *encryption.ClientInstance reverse *Reverse + + testpre uint32 + locker sync.Mutex + conns []stat.Connection } // New creates a new VLess outbound handler. @@ -105,6 +110,8 @@ func New(ctx context.Context, config *Config) (*Handler, error) { }() } + handler.testpre = a.Testpre + return handler, nil } @@ -128,15 +135,44 @@ 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.locker.Lock() + if h.conns == nil { + h.conns = make([]stat.Connection, 0) + go func() { + for { // TODO: close & inactive + time.Sleep(100 * time.Millisecond) // TODO: customize & randomize + h.locker.Lock() + if len(h.conns) >= int(h.testpre) { + h.locker.Unlock() + continue + } + h.locker.Unlock() + if conn, err := dialer.Dial(context.Background(), rec.Destination); err == nil { // TODO: timeout & concurrency? & ctx mitm? + h.locker.Lock() + h.conns = append(h.conns, conn) // TODO: vision paddings + h.locker.Unlock() + } + } + }() + } else if len(h.conns) > 0 { + conn = h.conns[0] + h.conns = h.conns[1:] + } + h.locker.Unlock() + } + + 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()