From d20d4289854e89bae4639b5589ec1428bd7247c4 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 1/7] 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 efb480a037be..2e5733050231 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 ac00ea53f2a1..ead3c7c27829 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 5822f5124548..2c59f0d02698 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 047311dd87e6..74d0e3dc11af 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 c425151feef5..add9ece0dc2d 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() From df4fb0061cc7054563edb74761ccebf1a0462063 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 4 Nov 2025 02:06:58 +0000 Subject: [PATCH 2/7] Avoid panic https://github.com/XTLS/Xray-core/pull/5270#issuecomment-3483155727 --- app/proxyman/outbound/handler.go | 8 ++++++-- proxy/vless/outbound/outbound.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 3c3c691853c6..eaa1b0b2f2f6 100644 --- a/app/proxyman/outbound/handler.go +++ b/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 } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index add9ece0dc2d..fc44ed573069 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -176,6 +176,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } defer conn.Close() + ob.Conn = conn // for Vision's pre-connect + iConn := conn if statConn, ok := iConn.(*stat.CounterConnection); ok { iConn = statConn.Connection From e681b3b19cfcc12d305106fb12740ecd9d0427d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Thu, 6 Nov 2025 11:45:56 +0000 Subject: [PATCH 3/7] Use channel for pool --- proxy/vless/outbound/outbound.go | 98 +++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index fc44ed573069..6bdbe776b2cf 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -54,9 +54,11 @@ type Handler struct { encryption *encryption.ClientInstance reverse *Reverse - testpre uint32 - locker sync.Mutex - conns []stat.Connection + testpre uint32 + initConns sync.Once + preConns chan stat.Connection + preConnWait chan struct{} + preConnStop chan struct{} } // New creates a new VLess outbound handler. @@ -117,6 +119,13 @@ func New(ctx context.Context, config *Config) (*Handler, error) { // Close implements common.Closable.Close(). func (h *Handler) Close() error { + if h.preConnStop != nil { + close(h.preConnStop) + for range h.testpre { + conn := <-h.preConns + common.CloseIfExists(conn) + } + } if h.reverse != nil { return h.reverse.Close() } @@ -136,30 +145,19 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte var conn stat.Connection 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.initConns.Do(func() { + h.preConns = make(chan stat.Connection, h.testpre) + h.preConnStop = make(chan struct{}) + go h.preConnWorker(dialer, rec.Destination) + }) + select { + case h.preConnWait <- struct{}{}: + default: + } + select { + case conn = <-h.preConns: + default: } - h.locker.Unlock() } if conn == nil { @@ -464,3 +462,51 @@ func (r *Reverse) Start() error { func (r *Reverse) Close() error { return r.monitorTask.Close() } + +func (h *Handler) preConnWorker(dialer internet.Dialer, dest net.Destination) { + // conn in conns may be nil + conns := make(chan stat.Connection) + dial := func() { + conn, err := dialer.Dial(context.Background(), dest) + if err != nil { + errors.LogError(context.Background(), "failed to dial VLESS pre connection: ", err) + common.CloseIfExists(conn) + } + conns <- conn + } + go func() { + go dial() // get a conn immediately + for range h.testpre - 1 { + select { + case <-h.preConnWait: + go dial() + case <-h.preConnStop: + return + } + } + }() + for { + select { + case conn := <-conns: + if conn != nil { + select { + case h.preConns <- conn: + case <-h.preConnStop: + common.CloseIfExists(conn) + return + } + go dial() + } else { + // sleep until next client try if dial failed + select { + case <-h.preConnWait: + go dial() + case <-h.preConnStop: + return + } + } + case <-h.preConnStop: + return + } + } +} From 902edd5adb2cd66a1a3d3dd53d10543cfe6cd9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 26 Nov 2025 21:11:54 +0000 Subject: [PATCH 4/7] Clear preConns in defer --- proxy/vless/outbound/outbound.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 6bdbe776b2cf..ea490f793187 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -121,10 +121,6 @@ func New(ctx context.Context, config *Config) (*Handler, error) { func (h *Handler) Close() error { if h.preConnStop != nil { close(h.preConnStop) - for range h.testpre { - conn := <-h.preConns - common.CloseIfExists(conn) - } } if h.reverse != nil { return h.reverse.Close() @@ -147,6 +143,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte if h.testpre > 0 && h.reverse == nil { h.initConns.Do(func() { h.preConns = make(chan stat.Connection, h.testpre) + h.preConnWait = make(chan struct{}) h.preConnStop = make(chan struct{}) go h.preConnWorker(dialer, rec.Destination) }) @@ -472,7 +469,13 @@ func (h *Handler) preConnWorker(dialer internet.Dialer, dest net.Destination) { errors.LogError(context.Background(), "failed to dial VLESS pre connection: ", err) common.CloseIfExists(conn) } - conns <- conn + select { + case <-h.preConnStop: + common.CloseIfExists(conn) + return + default: + conns <- conn + } } go func() { go dial() // get a conn immediately @@ -485,6 +488,16 @@ func (h *Handler) preConnWorker(dialer internet.Dialer, dest net.Destination) { } } }() + defer func() { + close(h.preConns) + for { + conn, ok := <-h.preConns + if !ok { + break + } + common.CloseIfExists(conn) + } + }() for { select { case conn := <-conns: From 28a8b040dce1c6511e67a8f0c4c4e091ee1d0335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Thu, 27 Nov 2025 11:44:53 +0000 Subject: [PATCH 5/7] remove manager --- proxy/vless/outbound/outbound.go | 126 ++++++++++++------------------- 1 file changed, 49 insertions(+), 77 deletions(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index ea490f793187..7d4d0379caca 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" "sync" + "sync/atomic" "time" "unsafe" @@ -16,6 +17,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" @@ -54,11 +56,13 @@ type Handler struct { encryption *encryption.ClientInstance reverse *Reverse - testpre uint32 - initConns sync.Once - preConns chan stat.Connection - preConnWait chan struct{} - preConnStop chan struct{} + testpre uint32 + initConns sync.Once + preConns chan stat.Connection + preConnWait chan struct{} + preConnWarmFinished atomic.Bool + preConnCtx context.Context + preConnStop context.CancelFunc } // New creates a new VLess outbound handler. @@ -120,7 +124,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) { // Close implements common.Closable.Close(). func (h *Handler) Close() error { if h.preConnStop != nil { - close(h.preConnStop) + h.preConnStop() } if h.reverse != nil { return h.reverse.Close() @@ -142,14 +146,18 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte if h.testpre > 0 && h.reverse == nil { h.initConns.Do(func() { - h.preConns = make(chan stat.Connection, h.testpre) - h.preConnWait = make(chan struct{}) - h.preConnStop = make(chan struct{}) - go h.preConnWorker(dialer, rec.Destination) + h.preConns = make(chan stat.Connection) + h.preConnWait = make(chan struct{}, h.testpre) + preConnCtx, preConnStop := context.WithCancel(context.Background()) + preConnCtx = xctx.ContextWithID(preConnCtx, session.NewID()) + h.preConnCtx = preConnCtx + h.preConnStop = preConnStop + for range h.testpre { + h.preConnWait <- struct{}{} + } }) - select { - case h.preConnWait <- struct{}{}: - default: + if !h.preConnWarmFinished.Load() { + h.tryStartDialPreConn(dialer, rec.Destination) } select { case conn = <-h.preConns: @@ -396,6 +404,34 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } +func (h *Handler) tryStartDialPreConn(dialer internet.Dialer, dest net.Destination) { + select { + case <-h.preConnWait: + go func() { + // TODO: Randomnized + time.Sleep(time.Second * 2) + for { + conn, err := dialer.Dial(h.preConnCtx, dest) + if err != nil { + errors.LogWarningInner(h.preConnCtx, err, "pre-connect failed") + } + select { + case h.preConns <- conn: + if err != nil { + // TODO: Randomnized + time.Sleep(time.Second * 2) + } + case <-h.preConnCtx.Done(): + common.CloseIfExists(conn) + return + } + } + }() + default: + h.preConnWarmFinished.Store(true) + } +} + type Reverse struct { tag string dispatcher routing.Dispatcher @@ -459,67 +495,3 @@ func (r *Reverse) Start() error { func (r *Reverse) Close() error { return r.monitorTask.Close() } - -func (h *Handler) preConnWorker(dialer internet.Dialer, dest net.Destination) { - // conn in conns may be nil - conns := make(chan stat.Connection) - dial := func() { - conn, err := dialer.Dial(context.Background(), dest) - if err != nil { - errors.LogError(context.Background(), "failed to dial VLESS pre connection: ", err) - common.CloseIfExists(conn) - } - select { - case <-h.preConnStop: - common.CloseIfExists(conn) - return - default: - conns <- conn - } - } - go func() { - go dial() // get a conn immediately - for range h.testpre - 1 { - select { - case <-h.preConnWait: - go dial() - case <-h.preConnStop: - return - } - } - }() - defer func() { - close(h.preConns) - for { - conn, ok := <-h.preConns - if !ok { - break - } - common.CloseIfExists(conn) - } - }() - for { - select { - case conn := <-conns: - if conn != nil { - select { - case h.preConns <- conn: - case <-h.preConnStop: - common.CloseIfExists(conn) - return - } - go dial() - } else { - // sleep until next client try if dial failed - select { - case <-h.preConnWait: - go dial() - case <-h.preConnStop: - return - } - } - case <-h.preConnStop: - return - } - } -} From 0012978da6dac36db106eadc6f215a908dea76ed Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 30 Nov 2025 09:40:39 +0000 Subject: [PATCH 6/7] simplify code --- proxy/vless/outbound/outbound.go | 74 ++++++++++---------------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 7d4d0379caca..f93b8a4d4b78 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -8,7 +8,6 @@ import ( "reflect" "strings" "sync" - "sync/atomic" "time" "unsafe" @@ -56,13 +55,9 @@ type Handler struct { encryption *encryption.ClientInstance reverse *Reverse - testpre uint32 - initConns sync.Once - preConns chan stat.Connection - preConnWait chan struct{} - preConnWarmFinished atomic.Bool - preConnCtx context.Context - preConnStop context.CancelFunc + testpre uint32 + initpre sync.Once + preConns chan stat.Connection } // New creates a new VLess outbound handler. @@ -123,8 +118,8 @@ func New(ctx context.Context, config *Config) (*Handler, error) { // Close implements common.Closable.Close(). func (h *Handler) Close() error { - if h.preConnStop != nil { - h.preConnStop() + if h.preConns != nil { + close(h.preConns) } if h.reverse != nil { return h.reverse.Close() @@ -145,23 +140,26 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte var conn stat.Connection if h.testpre > 0 && h.reverse == nil { - h.initConns.Do(func() { + h.initpre.Do(func() { h.preConns = make(chan stat.Connection) - h.preConnWait = make(chan struct{}, h.testpre) - preConnCtx, preConnStop := context.WithCancel(context.Background()) - preConnCtx = xctx.ContextWithID(preConnCtx, session.NewID()) - h.preConnCtx = preConnCtx - h.preConnStop = preConnStop - for range h.testpre { - h.preConnWait <- struct{}{} + 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 !h.preConnWarmFinished.Load() { - h.tryStartDialPreConn(dialer, rec.Destination) - } - select { - case conn = <-h.preConns: - default: + if conn = <-h.preConns; conn == nil { + return errors.New("closed handler").AtWarning() } } @@ -404,34 +402,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } -func (h *Handler) tryStartDialPreConn(dialer internet.Dialer, dest net.Destination) { - select { - case <-h.preConnWait: - go func() { - // TODO: Randomnized - time.Sleep(time.Second * 2) - for { - conn, err := dialer.Dial(h.preConnCtx, dest) - if err != nil { - errors.LogWarningInner(h.preConnCtx, err, "pre-connect failed") - } - select { - case h.preConns <- conn: - if err != nil { - // TODO: Randomnized - time.Sleep(time.Second * 2) - } - case <-h.preConnCtx.Done(): - common.CloseIfExists(conn) - return - } - } - }() - default: - h.preConnWarmFinished.Store(true) - } -} - type Reverse struct { tag string dispatcher routing.Dispatcher From 934beb20d475b7f27e52ce5a8b022b7c1e58ae0b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:41:45 +0000 Subject: [PATCH 7/7] Add testseed https://github.com/XTLS/Xray-core/pull/5270#issuecomment-3592424451 --- infra/conf/vless.go | 7 +++++++ proxy/proxy.go | 26 ++++++++++++++++---------- proxy/vless/account.go | 5 ++++- proxy/vless/account.pb.go | 25 +++++++++++++++++-------- proxy/vless/account.proto | 1 + proxy/vless/encoding/addons.go | 2 +- 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 2e5733050231..f482c4d80825 100644 --- a/infra/conf/vless.go +++ b/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`) } @@ -213,6 +218,7 @@ type VLessOutboundConfig struct { Encryption string `json:"encryption"` Reverse *vless.Reverse `json:"reverse"` Testpre uint32 `json:"testpre"` + Testseed []uint32 `json:"testseed"` Vnext []*VLessOutboundVnext `json:"vnext"` } @@ -260,6 +266,7 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { 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) diff --git a/proxy/proxy.go b/proxy/proxy.go index f759fdc0f164..d39c2df5cc7d 100644 --- a/proxy/proxy.go +++ b/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) } mb = ReshapeMultiBuffer(w.ctx, mb) @@ -364,13 +370,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 @@ -380,7 +386,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) @@ -422,20 +428,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") } diff --git a/proxy/vless/account.go b/proxy/vless/account.go index ead3c7c27829..2f617149d4e7 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -23,6 +23,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { Padding: a.Padding, Reverse: a.Reverse, Testpre: a.Testpre, + Testseed: a.Testseed, }, nil } @@ -40,7 +41,8 @@ type MemoryAccount struct { Reverse *Reverse - Testpre uint32 + Testpre uint32 + Testseed []uint32 } // Equals implements protocol.Account.Equals(). @@ -62,5 +64,6 @@ func (a *MemoryAccount) ToProto() proto.Message { Padding: a.Padding, Reverse: a.Reverse, Testpre: a.Testpre, + Testseed: a.Testseed, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index 2c59f0d02698..ce01ecae56d7 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -80,6 +80,7 @@ type Account struct { 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() { @@ -168,6 +169,13 @@ func (x *Account) GetTestpre() uint32 { 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{ @@ -175,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, 0xea, 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, @@ -190,13 +198,14 @@ var file_proxy_vless_account_proto_rawDesc = []byte{ 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, 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, + 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 ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 74d0e3dc11af..d0f1ee2259a5 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -24,4 +24,5 @@ message Account { Reverse reverse = 7; uint32 testpre = 8; + repeated uint32 testseed = 9; } diff --git a/proxy/vless/encoding/addons.go b/proxy/vless/encoding/addons.go index 77b09861109e..724de77716b7 100644 --- a/proxy/vless/encoding/addons.go +++ b/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 }