From cbade89ab11af26ba1e480a3688a6c205fa3c3f8 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:03:55 +0000 Subject: [PATCH] VLESS Encryption: Improve server-side tickets' expiration mechanism https://github.com/XTLS/Xray-core/pull/5067#issuecomment-3253717319 --- infra/conf/vless.go | 6 +-- proxy/vless/encryption/server.go | 78 ++++++++++++++++++++++++-------- proxy/vless/inbound/config.pb.go | 12 ++--- proxy/vless/inbound/config.proto | 4 +- proxy/vless/inbound/inbound.go | 3 ++ 5 files changed, 72 insertions(+), 31 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 3d7728df..999e3b9b 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -101,13 +101,13 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { if err != nil { return false } - config.SecondsFrom = uint32(i) - if len(t) > 1 { + config.SecondsFrom = int64(i) + if len(t) == 2 { i, err := strconv.Atoi(t[1]) if err != nil { return false } - config.SecondsTo = uint32(i) + config.SecondsTo = int64(i) } padding := 0 for _, r := range s[3:] { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 132cd1b0..e1d73716 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -28,16 +28,19 @@ type ServerInstance struct { Hash32s [][32]byte RelaysLength int XorMode uint32 - SecondsFrom uint32 - SecondsTo uint32 + SecondsFrom int64 + SecondsTo int64 PaddingLens [][3]int PaddingGaps [][3]int RWLock sync.RWMutex + Closed bool + Lasts map[int64][16]byte + Tickets [][16]byte Sessions map[[16]byte]*ServerSession } -func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, secondsFrom, secondsTo uint32, padding string) (err error) { +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) { if i.NfsSKeys != nil { return errors.New("already initialized") } @@ -68,8 +71,47 @@ func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, secondsFrom, seco i.XorMode = xorMode i.SecondsFrom = secondsFrom i.SecondsTo = secondsTo - i.Sessions = make(map[[16]byte]*ServerSession) - return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) + err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) + if err != nil { + return + } + if i.SecondsFrom > 0 || i.SecondsTo > 0 { + i.Lasts = make(map[int64][16]byte) + i.Tickets = make([][16]byte, 0, 1024) + i.Sessions = make(map[[16]byte]*ServerSession) + go func() { + for { + time.Sleep(time.Minute) + i.RWLock.Lock() + if i.Closed { + i.RWLock.Unlock() + return + } + minute := time.Now().Unix() / 60 + last := i.Lasts[minute] + delete(i.Lasts, minute) + delete(i.Lasts, minute-1) // for insurance + if last != [16]byte{} { + for j, ticket := range i.Tickets { + delete(i.Sessions, ticket) + if ticket == last { + i.Tickets = i.Tickets[j+1:] + break + } + } + } + i.RWLock.Unlock() + } + }() + } + return +} + +func (i *ServerInstance) Close() (err error) { + i.RWLock.Lock() + i.Closed = true + i.RWLock.Unlock() + return } func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) { @@ -224,25 +266,21 @@ func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn 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) - seconds := 0 + ticket := [16]byte{} + rand.Read(ticket[:]) + var seconds int64 if i.SecondsTo == 0 { - seconds = int(i.SecondsFrom) * int(crypto.RandBetween(50, 100)) / 100 + seconds = i.SecondsFrom * crypto.RandBetween(50, 100) / 100 } else { - seconds = int(crypto.RandBetween(int64(i.SecondsFrom), int64(i.SecondsTo))) + seconds = crypto.RandBetween(i.SecondsFrom, i.SecondsTo) } - copy(ticket, EncodeLength(int(seconds))) + copy(ticket[:], EncodeLength(int(seconds))) if seconds > 0 { i.RWLock.Lock() - i.Sessions[[16]byte(ticket)] = &ServerSession{PfsKey: pfsKey} + i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket + i.Tickets = append(i.Tickets, ticket) + i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey} i.RWLock.Unlock() - go func() { - time.Sleep(time.Duration(seconds)*time.Second + time.Minute) - i.RWLock.Lock() - delete(i.Sessions, [16]byte(ticket)) - i.RWLock.Unlock() - }() } pfsKeyExchangeLength := 1088 + 32 + 16 @@ -250,7 +288,7 @@ func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn 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) + c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil) padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:] c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) @@ -284,7 +322,7 @@ func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn } if i.XorMode == 2 { - c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0) + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0) } return c, nil } diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index 3a897411..f65da3c4 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -115,8 +115,8 @@ type Config struct { Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` Decryption string `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"` XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` - SecondsFrom uint32 `protobuf:"varint,5,opt,name=seconds_from,json=secondsFrom,proto3" json:"seconds_from,omitempty"` - SecondsTo uint32 `protobuf:"varint,6,opt,name=seconds_to,json=secondsTo,proto3" json:"seconds_to,omitempty"` + SecondsFrom int64 `protobuf:"varint,5,opt,name=seconds_from,json=secondsFrom,proto3" json:"seconds_from,omitempty"` + SecondsTo int64 `protobuf:"varint,6,opt,name=seconds_to,json=secondsTo,proto3" json:"seconds_to,omitempty"` Padding string `protobuf:"bytes,7,opt,name=padding,proto3" json:"padding,omitempty"` } @@ -178,14 +178,14 @@ func (x *Config) GetXorMode() uint32 { return 0 } -func (x *Config) GetSecondsFrom() uint32 { +func (x *Config) GetSecondsFrom() int64 { if x != nil { return x.SecondsFrom } return 0 } -func (x *Config) GetSecondsTo() uint32 { +func (x *Config) GetSecondsTo() int64 { if x != nil { return x.SecondsTo } @@ -228,10 +228,10 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 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, 0x21, 0x0a, 0x0c, 0x73, 0x65, - 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, + 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x74, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x09, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x54, 0x6f, 0x12, 0x18, 0x0a, 0x07, + 0x03, 0x52, 0x09, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x54, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x07, 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, diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto index febf2ef4..5a7e3e19 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -23,7 +23,7 @@ message Config { string decryption = 3; uint32 xorMode = 4; - uint32 seconds_from = 5; - uint32 seconds_to = 6; + int64 seconds_from = 5; + int64 seconds_to = 6; string padding = 7; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index b1b0a916..98db4f71 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -176,6 +176,9 @@ func isMuxAndNotXUDP(request *protocol.RequestHeader, first *buf.Buffer) bool { // Close implements common.Closable.Close(). func (h *Handler) Close() error { + if h.decryption != nil { + h.decryption.Close() + } return errors.Combine(common.Close(h.validator)) }