From 2f0596840cd25cb35c6149cecfe1f3bb791a4eaa Mon Sep 17 00:00:00 2001
From: RPRX <63339210+RPRX@users.noreply.github.com>
Date: Thu, 28 Aug 2025 04:55:36 +0000
Subject: [PATCH 01/10] VLESS protocol: Add lightweight Post-Quantum
ML-KEM-768-based PFS 1-RTT / anti-replay 0-RTT AEAD Encryption (#5067)
https://opensea.io/collection/vless
---
common/protocol/headers.go | 14 +-
infra/conf/vless.go | 85 +++++++++-
main/commands/all/commands.go | 1 +
main/commands/all/curve25519.go | 47 +++---
main/commands/all/mldsa65.go | 14 +-
main/commands/all/mlkem768.go | 50 ++++++
main/commands/all/uuid.go | 4 +-
main/commands/all/wg.go | 4 +-
main/commands/all/x25519.go | 4 +-
proxy/proxy.go | 45 +++--
proxy/vless/account.go | 8 +-
proxy/vless/account.pb.go | 46 +++--
proxy/vless/account.proto | 4 +-
proxy/vless/encoding/encoding.go | 24 ++-
proxy/vless/encryption/client.go | 202 ++++++++++++++++++++++
proxy/vless/encryption/common.go | 212 +++++++++++++++++++++++
proxy/vless/encryption/server.go | 282 +++++++++++++++++++++++++++++++
proxy/vless/encryption/xor.go | 93 ++++++++++
proxy/vless/inbound/config.pb.go | 59 ++++---
proxy/vless/inbound/config.proto | 9 +-
proxy/vless/inbound/inbound.go | 37 +++-
proxy/vless/outbound/outbound.go | 36 +++-
22 files changed, 1161 insertions(+), 119 deletions(-)
create mode 100644 main/commands/all/mlkem768.go
create mode 100644 proxy/vless/encryption/client.go
create mode 100644 proxy/vless/encryption/common.go
create mode 100644 proxy/vless/encryption/server.go
create mode 100644 proxy/vless/encryption/xor.go
diff --git a/common/protocol/headers.go b/common/protocol/headers.go
index 261e21d93413..fb785d73787d 100644
--- a/common/protocol/headers.go
+++ b/common/protocol/headers.go
@@ -79,20 +79,18 @@ type CommandSwitchAccount struct {
}
var (
- hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
+ // Keep in sync with crypto/tls/cipher_suites.go.
+ hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
- // Keep in sync with crypto/aes/cipher_s390x.go.
- hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR &&
- (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM)
+ hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH
+ hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
- hasAESGCMHardwareSupport = runtime.GOARCH == "amd64" && hasGCMAsmAMD64 ||
- runtime.GOARCH == "arm64" && hasGCMAsmARM64 ||
- runtime.GOARCH == "s390x" && hasGCMAsmS390X
+ HasAESGCMHardwareSupport = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)
func (sc *SecurityConfig) GetSecurityType() SecurityType {
if sc == nil || sc.Type == SecurityType_AUTO {
- if hasAESGCMHardwareSupport {
+ if HasAESGCMHardwareSupport {
return SecurityType_AES128_GCM
}
return SecurityType_CHACHA20_POLY1305
diff --git a/infra/conf/vless.go b/infra/conf/vless.go
index 1a7341b30d68..5d10cc97a393 100644
--- a/infra/conf/vless.go
+++ b/infra/conf/vless.go
@@ -1,6 +1,7 @@
package conf
import (
+ "encoding/base64"
"encoding/json"
"path/filepath"
"runtime"
@@ -80,10 +81,45 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
config.Clients[idx] = user
}
- if c.Decryption != "none" {
- return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`)
- }
config.Decryption = c.Decryption
+ if !func() bool {
+ s := strings.Split(config.Decryption, ".")
+ if len(s) < 4 || s[0] != "mlkem768x25519plus" {
+ return false
+ }
+ switch s[1] {
+ case "native":
+ case "xorpub":
+ config.XorMode = 1
+ case "random":
+ config.XorMode = 2
+ default:
+ return false
+ }
+ if s[2] != "1rtt" {
+ t := strings.TrimSuffix(s[2], "s")
+ if t == s[2] {
+ return false
+ }
+ i, err := strconv.Atoi(t)
+ if err != nil {
+ return false
+ }
+ config.Seconds = uint32(i)
+ }
+ for i := 3; i < len(s); i++ {
+ if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 64 {
+ return false
+ }
+ }
+ config.Decryption = config.Decryption[27+len(s[2]):]
+ return true
+ }() && config.Decryption != "none" {
+ if config.Decryption == "" {
+ return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`)
+ }
+ return nil, errors.New(`VLESS settings: unsupported "decryption": ` + config.Decryption)
+ }
for _, fb := range c.Fallbacks {
var i uint16
@@ -155,16 +191,16 @@ type VLessOutboundConfig struct {
func (c *VLessOutboundConfig) Build() (proto.Message, error) {
config := new(outbound.Config)
- if len(c.Vnext) == 0 {
- return nil, errors.New(`VLESS settings: "vnext" is empty`)
+ if len(c.Vnext) != 1 {
+ return nil, errors.New(`VLESS settings: "vnext" should have one and only one member`)
}
config.Vnext = make([]*protocol.ServerEndpoint, len(c.Vnext))
for idx, rec := range c.Vnext {
if rec.Address == nil {
return nil, errors.New(`VLESS vnext: "address" is not set`)
}
- if len(rec.Users) == 0 {
- return nil, errors.New(`VLESS vnext: "users" is empty`)
+ if len(rec.Users) != 1 {
+ return nil, errors.New(`VLESS vnext: "users" should have one and only one member`)
}
spec := &protocol.ServerEndpoint{
Address: rec.Address.Build(),
@@ -193,8 +229,39 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`)
}
- if account.Encryption != "none" {
- return nil, errors.New(`VLESS users: please add/set "encryption":"none" for every user`)
+ if !func() bool {
+ s := strings.Split(account.Encryption, ".")
+ if len(s) < 4 || s[0] != "mlkem768x25519plus" {
+ return false
+ }
+ switch s[1] {
+ case "native":
+ case "xorpub":
+ account.XorMode = 1
+ case "random":
+ account.XorMode = 2
+ default:
+ return false
+ }
+ switch s[2] {
+ case "1rtt":
+ case "0rtt":
+ account.Seconds = 1
+ default:
+ return false
+ }
+ for i := 3; i < len(s); i++ {
+ if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 1184 {
+ return false
+ }
+ }
+ account.Encryption = account.Encryption[27+len(s[2]):]
+ return true
+ }() && account.Encryption != "none" {
+ if account.Encryption == "" {
+ return nil, errors.New(`VLESS users: please add/set "encryption":"none" for every user`)
+ }
+ return nil, errors.New(`VLESS users: unsupported "encryption": ` + account.Encryption)
}
user.Account = serial.ToTypedMessage(account)
diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go
index 3667a1d8d39e..9f8270f94495 100644
--- a/main/commands/all/commands.go
+++ b/main/commands/all/commands.go
@@ -17,5 +17,6 @@ func init() {
cmdX25519,
cmdWG,
cmdMLDSA65,
+ cmdMLKEM768,
)
}
diff --git a/main/commands/all/curve25519.go b/main/commands/all/curve25519.go
index bb706c6c2ac5..16ca8c7cf648 100644
--- a/main/commands/all/curve25519.go
+++ b/main/commands/all/curve25519.go
@@ -1,17 +1,15 @@
package all
import (
+ "crypto/ecdh"
"crypto/rand"
"encoding/base64"
"fmt"
- "golang.org/x/crypto/curve25519"
+ "lukechampine.com/blake3"
)
func Curve25519Genkey(StdEncoding bool, input_base64 string) {
- var output string
- var err error
- var privateKey, publicKey []byte
var encoding *base64.Encoding
if *input_stdEncoding || StdEncoding {
encoding = base64.StdEncoding
@@ -19,40 +17,35 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) {
encoding = base64.RawURLEncoding
}
+ var privateKey []byte
if len(input_base64) > 0 {
- privateKey, err = encoding.DecodeString(input_base64)
- if err != nil {
- output = err.Error()
- goto out
- }
- if len(privateKey) != curve25519.ScalarSize {
- output = "Invalid length of private key."
- goto out
+ privateKey, _ = encoding.DecodeString(input_base64)
+ if len(privateKey) != 32 {
+ fmt.Println("Invalid length of X25519 private key.")
+ return
}
}
-
if privateKey == nil {
- privateKey = make([]byte, curve25519.ScalarSize)
- if _, err = rand.Read(privateKey); err != nil {
- output = err.Error()
- goto out
- }
+ privateKey = make([]byte, 32)
+ rand.Read(privateKey)
}
// Modify random bytes using algorithm described at:
- // https://cr.yp.to/ecdh.html.
+ // https://cr.yp.to/ecdh.html
+ // (Just to make sure printing the real private key)
privateKey[0] &= 248
privateKey[31] &= 127
privateKey[31] |= 64
- if publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint); err != nil {
- output = err.Error()
- goto out
+ key, err := ecdh.X25519().NewPrivateKey(privateKey)
+ if err != nil {
+ fmt.Println(err.Error())
+ return
}
-
- output = fmt.Sprintf("Private key: %v\nPublic key: %v",
+ password := key.PublicKey().Bytes()
+ hash32 := blake3.Sum256(password)
+ fmt.Printf("PrivateKey: %v\nPassword: %v\nHash32: %v",
encoding.EncodeToString(privateKey),
- encoding.EncodeToString(publicKey))
-out:
- fmt.Println(output)
+ encoding.EncodeToString(password),
+ encoding.EncodeToString(hash32[:]))
}
diff --git a/main/commands/all/mldsa65.go b/main/commands/all/mldsa65.go
index fe0f5eb42588..495fb088e948 100644
--- a/main/commands/all/mldsa65.go
+++ b/main/commands/all/mldsa65.go
@@ -11,9 +11,9 @@ import (
var cmdMLDSA65 = &base.Command{
UsageLine: `{{.Exec}} mldsa65 [-i "seed (base64.RawURLEncoding)"]`,
- Short: `Generate key pair for ML-DSA-65 post-quantum signature`,
+ Short: `Generate key pair for ML-DSA-65 post-quantum signature (REALITY)`,
Long: `
-Generate key pair for ML-DSA-65 post-quantum signature.
+Generate key pair for ML-DSA-65 post-quantum signature (REALITY).
Random: {{.Exec}} mldsa65
@@ -25,12 +25,16 @@ func init() {
cmdMLDSA65.Run = executeMLDSA65 // break init loop
}
-var input_seed = cmdMLDSA65.Flag.String("i", "", "")
+var input_mldsa65 = cmdMLDSA65.Flag.String("i", "", "")
func executeMLDSA65(cmd *base.Command, args []string) {
var seed [32]byte
- if len(*input_seed) > 0 {
- s, _ := base64.RawURLEncoding.DecodeString(*input_seed)
+ if len(*input_mldsa65) > 0 {
+ s, _ := base64.RawURLEncoding.DecodeString(*input_mldsa65)
+ if len(s) != 32 {
+ fmt.Println("Invalid length of ML-DSA-65 seed.")
+ return
+ }
seed = [32]byte(s)
} else {
rand.Read(seed[:])
diff --git a/main/commands/all/mlkem768.go b/main/commands/all/mlkem768.go
new file mode 100644
index 000000000000..0f6e707b53ab
--- /dev/null
+++ b/main/commands/all/mlkem768.go
@@ -0,0 +1,50 @@
+package all
+
+import (
+ "crypto/mlkem"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+
+ "github.com/xtls/xray-core/main/commands/base"
+ "lukechampine.com/blake3"
+)
+
+var cmdMLKEM768 = &base.Command{
+ UsageLine: `{{.Exec}} mlkem768 [-i "seed (base64.RawURLEncoding)"]`,
+ Short: `Generate key pair for ML-KEM-768 post-quantum key exchange (VLESS)`,
+ Long: `
+Generate key pair for ML-KEM-768 post-quantum key exchange (VLESS).
+
+Random: {{.Exec}} mlkem768
+
+From seed: {{.Exec}} mlkem768 -i "seed (base64.RawURLEncoding)"
+`,
+}
+
+func init() {
+ cmdMLKEM768.Run = executeMLKEM768 // break init loop
+}
+
+var input_mlkem768 = cmdMLKEM768.Flag.String("i", "", "")
+
+func executeMLKEM768(cmd *base.Command, args []string) {
+ var seed [64]byte
+ if len(*input_mlkem768) > 0 {
+ s, _ := base64.RawURLEncoding.DecodeString(*input_mlkem768)
+ if len(s) != 64 {
+ fmt.Println("Invalid length of ML-KEM-768 seed.")
+ return
+ }
+ seed = [64]byte(s)
+ } else {
+ rand.Read(seed[:])
+ }
+ key, _ := mlkem.NewDecapsulationKey768(seed[:])
+ client := key.EncapsulationKey().Bytes()
+ hash32 := blake3.Sum256(client)
+ fmt.Printf("Seed: %v\nClient: %v\nHash32: %v",
+ base64.RawURLEncoding.EncodeToString(seed[:]),
+ base64.RawURLEncoding.EncodeToString(client),
+ base64.RawURLEncoding.EncodeToString(hash32[:]))
+}
diff --git a/main/commands/all/uuid.go b/main/commands/all/uuid.go
index b01e88f058f9..1fe27bf54a70 100644
--- a/main/commands/all/uuid.go
+++ b/main/commands/all/uuid.go
@@ -9,9 +9,9 @@ import (
var cmdUUID = &base.Command{
UsageLine: `{{.Exec}} uuid [-i "example"]`,
- Short: `Generate UUIDv4 or UUIDv5`,
+ Short: `Generate UUIDv4 or UUIDv5 (VLESS)`,
Long: `
-Generate UUIDv4 or UUIDv5.
+Generate UUIDv4 or UUIDv5 (VLESS).
UUIDv4 (random): {{.Exec}} uuid
diff --git a/main/commands/all/wg.go b/main/commands/all/wg.go
index 70da46682b11..1de0e515ee9c 100644
--- a/main/commands/all/wg.go
+++ b/main/commands/all/wg.go
@@ -6,9 +6,9 @@ import (
var cmdWG = &base.Command{
UsageLine: `{{.Exec}} wg [-i "private key (base64.StdEncoding)"]`,
- Short: `Generate key pair for wireguard key exchange`,
+ Short: `Generate key pair for X25519 key exchange (WireGuard)`,
Long: `
-Generate key pair for wireguard key exchange.
+Generate key pair for X25519 key exchange (WireGuard).
Random: {{.Exec}} wg
diff --git a/main/commands/all/x25519.go b/main/commands/all/x25519.go
index 73f669b269f2..7ef23f032bab 100644
--- a/main/commands/all/x25519.go
+++ b/main/commands/all/x25519.go
@@ -6,9 +6,9 @@ import (
var cmdX25519 = &base.Command{
UsageLine: `{{.Exec}} x25519 [-i "private key (base64.RawURLEncoding)"] [--std-encoding]`,
- Short: `Generate key pair for x25519 key exchange`,
+ Short: `Generate key pair for X25519 key exchange (VLESS, REALITY)`,
Long: `
-Generate key pair for x25519 key exchange.
+Generate key pair for X25519 key exchange (VLESS, REALITY).
Random: {{.Exec}} x25519
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 3fec31af94a9..049d9fbdbb74 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -25,6 +25,7 @@ import (
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
+ "github.com/xtls/xray-core/proxy/vless/encryption"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
@@ -524,24 +525,33 @@ func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx conte
}
}
-// UnwrapRawConn support unwrap stats, tls, utls, reality, proxyproto, uds-wrapper conn and get raw tcp/uds conn from it
+// UnwrapRawConn support unwrap encryption, stats, tls, utls, reality, proxyproto, uds-wrapper conn and get raw tcp/uds conn from it
func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) {
var readCounter, writerCounter stats.Counter
if conn != nil {
- statConn, ok := conn.(*stat.CounterConnection)
- if ok {
+ isEncryption := false
+ if commonConn, ok := conn.(*encryption.CommonConn); ok {
+ conn = commonConn.Conn
+ isEncryption = true
+ }
+ if xorConn, ok := conn.(*encryption.XorConn); ok {
+ return xorConn, nil, nil // full-random xorConn should not be penetrated
+ }
+ if statConn, ok := conn.(*stat.CounterConnection); ok {
conn = statConn.Connection
readCounter = statConn.ReadCounter
writerCounter = statConn.WriteCounter
}
- if xc, ok := conn.(*tls.Conn); ok {
- conn = xc.NetConn()
- } else if utlsConn, ok := conn.(*tls.UConn); ok {
- conn = utlsConn.NetConn()
- } else if realityConn, ok := conn.(*reality.Conn); ok {
- conn = realityConn.NetConn()
- } else if realityUConn, ok := conn.(*reality.UConn); ok {
- conn = realityUConn.NetConn()
+ if !isEncryption { // avoids double penetration
+ if xc, ok := conn.(*tls.Conn); ok {
+ conn = xc.NetConn()
+ } else if utlsConn, ok := conn.(*tls.UConn); ok {
+ conn = utlsConn.NetConn()
+ } else if realityConn, ok := conn.(*reality.Conn); ok {
+ conn = realityConn.NetConn()
+ } else if realityUConn, ok := conn.(*reality.UConn); ok {
+ conn = realityUConn.NetConn()
+ }
}
if pc, ok := conn.(*proxyproto.Conn); ok {
conn = pc.Raw()
@@ -632,9 +642,20 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net
}
func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, readCounter stats.Counter) error {
- errors.LogInfo(ctx, "CopyRawConn readv")
+ errors.LogInfo(ctx, "CopyRawConn (maybe) readv")
if err := buf.Copy(reader, writer, buf.UpdateActivity(timer), buf.AddToStatCounter(readCounter)); err != nil {
return errors.New("failed to process response").Base(err)
}
return nil
}
+
+func IsRAWTransport(conn stat.Connection) bool {
+ iConn := conn
+ if statConn, ok := iConn.(*stat.CounterConnection); ok {
+ iConn = statConn.Connection
+ }
+ _, ok1 := iConn.(*proxyproto.Conn)
+ _, ok2 := iConn.(*net.TCPConn)
+ _, ok3 := iConn.(*internet.UnixConnWrapper)
+ return ok1 || ok2 || ok3
+}
diff --git a/proxy/vless/account.go b/proxy/vless/account.go
index c22cfe16203b..b1e09619c2e4 100644
--- a/proxy/vless/account.go
+++ b/proxy/vless/account.go
@@ -18,6 +18,8 @@ func (a *Account) AsAccount() (protocol.Account, error) {
ID: protocol.NewID(id),
Flow: a.Flow, // needs parser here?
Encryption: a.Encryption, // needs parser here?
+ XorMode: a.XorMode,
+ Seconds: a.Seconds,
}, nil
}
@@ -27,8 +29,10 @@ type MemoryAccount struct {
ID *protocol.ID
// Flow of the account. May be "xtls-rprx-vision".
Flow string
- // Encryption of the account. Used for client connections, and only accepts "none" for now.
+
Encryption string
+ XorMode uint32
+ Seconds uint32
}
// Equals implements protocol.Account.Equals().
@@ -45,5 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message {
Id: a.ID.String(),
Flow: a.Flow,
Encryption: a.Encryption,
+ XorMode: a.XorMode,
+ Seconds: a.Seconds,
}
}
diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go
index fd5d4518468d..6048dc4e4358 100644
--- a/proxy/vless/account.pb.go
+++ b/proxy/vless/account.pb.go
@@ -28,9 +28,10 @@ type Account struct {
// ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57".
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Flow settings. May be "xtls-rprx-vision".
- Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"`
- // Encryption settings. Only applies to client side, and only accepts "none" for now.
+ Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"`
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"`
}
func (x *Account) Reset() {
@@ -84,23 +85,40 @@ func (x *Account) GetEncryption() string {
return ""
}
+func (x *Account) GetXorMode() uint32 {
+ if x != nil {
+ return x.XorMode
+ }
+ return 0
+}
+
+func (x *Account) GetSeconds() uint32 {
+ if x != nil {
+ return x.Seconds
+ }
+ return 0
+}
+
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, 0x4d, 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, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 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,
+ 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x81, 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, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x0a, 0x65, 0x6e, 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, 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,
}
var (
diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto
index 51d2cb7deaaf..ebb1feff0fa4 100644
--- a/proxy/vless/account.proto
+++ b/proxy/vless/account.proto
@@ -11,6 +11,8 @@ message Account {
string id = 1;
// Flow settings. May be "xtls-rprx-vision".
string flow = 2;
- // Encryption settings. Only applies to client side, and only accepts "none" for now.
+
string encryption = 3;
+ uint32 xorMode = 4;
+ uint32 seconds = 5;
}
diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go
index 61bfb911b29f..1176a960399b 100644
--- a/proxy/vless/encoding/encoding.go
+++ b/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, 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, peerCache *[]byte, 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,15 +194,21 @@ 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 TLS Conn's input and rawInput
- if inputBuffer, err := buf.ReadFrom(input); err == nil {
- if !inputBuffer.IsEmpty() {
- buffer, _ = buf.MergeMulti(buffer, inputBuffer)
+ // 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)
}
- }
- if rawInputBuffer, err := buf.ReadFrom(rawInput); err == nil {
- if !rawInputBuffer.IsEmpty() {
- buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
+ } 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)
+ }
}
}
}
diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go
new file mode 100644
index 000000000000..301c23280dfa
--- /dev/null
+++ b/proxy/vless/encryption/client.go
@@ -0,0 +1,202 @@
+package encryption
+
+import (
+ "crypto/cipher"
+ "crypto/ecdh"
+ "crypto/mlkem"
+ "crypto/rand"
+ "io"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/xtls/xray-core/common/crypto"
+ "github.com/xtls/xray-core/common/errors"
+ "lukechampine.com/blake3"
+)
+
+type ClientInstance struct {
+ NfsPKeys []any
+ NfsPKeysBytes [][]byte
+ Hash32s [][32]byte
+ RelaysLength int
+ XorMode uint32
+ Seconds uint32
+
+ RWLock sync.RWMutex
+ Expire time.Time
+ PfsKey []byte
+ Ticket []byte
+}
+
+func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
+ if i.NfsPKeys != nil {
+ err = errors.New("already initialized")
+ return
+ }
+ l := len(nfsPKeysBytes)
+ if l == 0 {
+ err = errors.New("empty nfsPKeysBytes")
+ return
+ }
+ i.NfsPKeys = make([]any, l)
+ i.NfsPKeysBytes = nfsPKeysBytes
+ i.Hash32s = make([][32]byte, l)
+ for j, k := range nfsPKeysBytes {
+ if len(k) == 32 {
+ if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil {
+ return
+ }
+ i.RelaysLength += 32 + 32
+ } else {
+ if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil {
+ return
+ }
+ i.RelaysLength += 1088 + 32
+ }
+ i.Hash32s[j] = blake3.Sum256(k)
+ }
+ i.RelaysLength -= 32
+ i.XorMode = xorMode
+ i.Seconds = seconds
+ return
+}
+
+func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
+ if i.NfsPKeys == nil {
+ return nil, errors.New("uninitialized")
+ }
+ c := NewCommonConn(conn)
+
+ ivAndRealysLength := 16 + i.RelaysLength
+ pfsKeyExchangeLength := 18 + 1184 + 32 + 16
+ paddingLength := int(crypto.RandBetween(100, 1000))
+ clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength)
+
+ iv := clientHello[:16]
+ rand.Read(iv)
+ relays := clientHello[16:ivAndRealysLength]
+ var nfsKey []byte
+ var lastCTR cipher.Stream
+ for j, k := range i.NfsPKeys {
+ var index = 32
+ if k, ok := k.(*ecdh.PublicKey); ok {
+ privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
+ copy(relays, privateKey.PublicKey().Bytes())
+ var err error
+ nfsKey, err = privateKey.ECDH(k)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if k, ok := k.(*mlkem.EncapsulationKey768); ok {
+ var ciphertext []byte
+ nfsKey, ciphertext = k.Encapsulate()
+ copy(relays, ciphertext)
+ index = 1088
+ }
+ if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values
+ NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes
+ }
+ if lastCTR != nil {
+ lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable
+ }
+ if j == len(i.NfsPKeys)-1 {
+ break
+ }
+ lastCTR = NewCTR(nfsKey, iv)
+ lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:])
+ relays = relays[index+32:]
+ }
+ nfsGCM := NewGCM(iv, nfsKey)
+
+ if i.Seconds > 0 {
+ i.RWLock.RLock()
+ if time.Now().Before(i.Expire) {
+ c.Client = i
+ c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection
+ nfsGCM.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil)
+ nfsGCM.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil)
+ i.RWLock.RUnlock()
+ c.PreWrite = clientHello[:ivAndRealysLength+18+32]
+ c.GCM = NewGCM(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey)
+ if i.XorMode == 2 {
+ c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16)
+ }
+ return c, nil
+ }
+ i.RWLock.RUnlock()
+ }
+
+ pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength]
+ nfsGCM.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil)
+ mlkem768DKey, _ := mlkem.GenerateKey768()
+ x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
+ pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...)
+ nfsGCM.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil)
+
+ padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:]
+ nfsGCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
+ nfsGCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
+
+ if _, err := conn.Write(clientHello); err != nil {
+ return nil, err
+ }
+ // 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 {
+ return nil, err
+ }
+ nfsGCM.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil)
+ mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088])
+ if err != nil {
+ return nil, err
+ }
+ peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32])
+ if err != nil {
+ return nil, err
+ }
+ x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
+ if err != nil {
+ return nil, err
+ }
+ pfsKey := make([]byte, 32+32) // no more capacity
+ copy(pfsKey, mlkem768Key)
+ copy(pfsKey[32:], x25519Key)
+ c.UnitedKey = append(pfsKey, nfsKey...)
+ c.GCM = NewGCM(pfsPublicKey, c.UnitedKey)
+ c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1088+32], c.UnitedKey)
+
+ encryptedTicket := make([]byte, 32)
+ if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
+ return nil, err
+ }
+ if _, err := c.PeerGCM.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil {
+ return nil, err
+ }
+ seconds := DecodeLength(encryptedTicket)
+
+ if i.Seconds > 0 && seconds > 0 {
+ i.RWLock.Lock()
+ i.Expire = time.Now().Add(time.Duration(seconds) * time.Second)
+ i.PfsKey = pfsKey
+ i.Ticket = encryptedTicket[:16]
+ i.RWLock.Unlock()
+ }
+
+ encryptedLength := make([]byte, 18)
+ if _, err := io.ReadFull(conn, encryptedLength); err != nil {
+ return nil, err
+ }
+ if _, err := c.PeerGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
+ return nil, err
+ }
+ length := DecodeLength(encryptedLength[:2])
+ c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern
+
+ if i.XorMode == 2 {
+ c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length)
+ }
+ return c, nil
+}
diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go
new file mode 100644
index 000000000000..6f914c8db774
--- /dev/null
+++ b/proxy/vless/encryption/common.go
@@ -0,0 +1,212 @@
+package encryption
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "fmt"
+ "io"
+ "net"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xtls/xray-core/common/errors"
+ "lukechampine.com/blake3"
+)
+
+var OutBytesPool = sync.Pool{
+ New: func() any {
+ return make([]byte, 5+8192+16)
+ },
+}
+
+type CommonConn struct {
+ net.Conn
+ Client *ClientInstance
+ UnitedKey []byte
+ PreWrite []byte
+ GCM *GCM
+ PeerGCM *GCM
+ PeerPadding []byte
+ PeerInBytes []byte
+ PeerCache []byte
+}
+
+func NewCommonConn(conn net.Conn) *CommonConn {
+ return &CommonConn{
+ Conn: conn,
+ PeerInBytes: make([]byte, 5+17000), // no need to use sync.Pool, because we are always reading
+ }
+}
+
+func (c *CommonConn) Write(b []byte) (int, error) {
+ if len(b) == 0 {
+ return 0, nil
+ }
+ outBytes := OutBytesPool.Get().([]byte)
+ defer OutBytesPool.Put(outBytes)
+ for n := 0; n < len(b); {
+ b := b[n:]
+ if len(b) > 8192 {
+ b = b[:8192] // for avoiding another copy() in peer's Read()
+ }
+ n += len(b)
+ headerAndData := outBytes[:5+len(b)+16]
+ EncodeHeader(headerAndData, len(b)+16)
+ max := false
+ if bytes.Equal(c.GCM.Nonce[:], MaxNonce) {
+ max = true
+ }
+ c.GCM.Seal(headerAndData[:5], nil, b, headerAndData[:5])
+ if max {
+ c.GCM = NewGCM(headerAndData, c.UnitedKey)
+ }
+ if c.PreWrite != nil {
+ headerAndData = append(c.PreWrite, headerAndData...)
+ c.PreWrite = nil
+ }
+ if _, err := c.Conn.Write(headerAndData); err != nil {
+ return 0, err
+ }
+ }
+ return len(b), nil
+}
+
+func (c *CommonConn) Read(b []byte) (int, error) {
+ if len(b) == 0 {
+ return 0, nil
+ }
+ if c.PeerGCM == nil { // client's 0-RTT
+ serverRandom := make([]byte, 16)
+ if _, err := io.ReadFull(c.Conn, serverRandom); err != nil {
+ return 0, err
+ }
+ c.PeerGCM = NewGCM(serverRandom, c.UnitedKey)
+ if xorConn, ok := c.Conn.(*XorConn); ok {
+ xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom)
+ }
+ }
+ if c.PeerPadding != nil { // client's 1-RTT
+ if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil {
+ return 0, err
+ }
+ if _, err := c.PeerGCM.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil {
+ return 0, err
+ }
+ c.PeerPadding = nil
+ }
+ if len(c.PeerCache) > 0 {
+ n := copy(b, c.PeerCache)
+ c.PeerCache = c.PeerCache[n:]
+ return n, nil
+ }
+ peerHeader := c.PeerInBytes[:5]
+ if _, err := io.ReadFull(c.Conn, peerHeader); err != nil {
+ return 0, err
+ }
+ l, err := DecodeHeader(c.PeerInBytes[:5]) // l: 17~17000
+ if err != nil {
+ if c.Client != nil && strings.Contains(err.Error(), "invalid header: ") { // client's 0-RTT
+ c.Client.RWLock.Lock()
+ if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) {
+ c.Client.Expire = time.Now() // expired
+ }
+ c.Client.RWLock.Unlock()
+ return 0, errors.New("new handshake needed")
+ }
+ return 0, err
+ }
+ c.Client = nil
+ peerData := c.PeerInBytes[5 : 5+l]
+ if _, err := io.ReadFull(c.Conn, peerData); err != nil {
+ return 0, err
+ }
+ dst := peerData[:l-16]
+ if len(dst) <= len(b) {
+ dst = b[:len(dst)] // avoids another copy()
+ }
+ var newGCM *GCM
+ if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) {
+ newGCM = NewGCM(c.PeerInBytes[:5+l], c.UnitedKey)
+ }
+ _, err = c.PeerGCM.Open(dst[:0], nil, peerData, peerHeader)
+ if newGCM != nil {
+ c.PeerGCM = newGCM
+ }
+ if err != nil {
+ return 0, err
+ }
+ if len(dst) > len(b) {
+ c.PeerCache = dst[copy(b, dst):]
+ dst = b // for len(dst)
+ }
+ return len(dst), nil
+}
+
+type GCM struct {
+ cipher.AEAD
+ Nonce [12]byte
+}
+
+func NewGCM(ctx, key []byte) *GCM {
+ k := make([]byte, 32)
+ blake3.DeriveKey(k, string(ctx), key)
+ block, _ := aes.NewCipher(k)
+ aead, _ := cipher.NewGCM(block)
+ return &GCM{AEAD: aead}
+ //chacha20poly1305.New()
+}
+
+func (a *GCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
+ if nonce == nil {
+ nonce = IncreaseNonce(a.Nonce[:])
+ }
+ return a.AEAD.Seal(dst, nonce, plaintext, additionalData)
+}
+
+func (a *GCM) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
+ if nonce == nil {
+ nonce = IncreaseNonce(a.Nonce[:])
+ }
+ return a.AEAD.Open(dst, nonce, ciphertext, additionalData)
+}
+
+func IncreaseNonce(nonce []byte) []byte {
+ for i := range 12 {
+ nonce[11-i]++
+ if nonce[11-i] != 0 {
+ break
+ }
+ }
+ return nonce
+}
+
+var MaxNonce = bytes.Repeat([]byte{255}, 12)
+
+func EncodeLength(l int) []byte {
+ return []byte{byte(l >> 8), byte(l)}
+}
+
+func DecodeLength(b []byte) int {
+ return int(b[0])<<8 | int(b[1])
+}
+
+func EncodeHeader(h []byte, l int) {
+ h[0] = 23
+ h[1] = 3
+ h[2] = 3
+ h[3] = byte(l >> 8)
+ h[4] = byte(l)
+}
+
+func DecodeHeader(h []byte) (l int, err error) {
+ l = int(h[3])<<8 | int(h[4])
+ if h[0] != 23 || h[1] != 3 || h[2] != 3 {
+ 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()
+ }
+ return
+}
diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go
new file mode 100644
index 000000000000..8594fd252776
--- /dev/null
+++ b/proxy/vless/encryption/server.go
@@ -0,0 +1,282 @@
+package encryption
+
+import (
+ "bytes"
+ "crypto/cipher"
+ "crypto/ecdh"
+ "crypto/mlkem"
+ "crypto/rand"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/xtls/xray-core/common/crypto"
+ "github.com/xtls/xray-core/common/errors"
+ "lukechampine.com/blake3"
+)
+
+type ServerSession struct {
+ Expire time.Time
+ PfsKey []byte
+ NfsKeys sync.Map
+}
+
+type ServerInstance struct {
+ NfsSKeys []any
+ NfsPKeysBytes [][]byte
+ Hash32s [][32]byte
+ RelaysLength int
+ XorMode uint32
+ Seconds uint32
+
+ RWLock sync.RWMutex
+ Sessions map[[16]byte]*ServerSession
+ Closed bool
+}
+
+func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
+ if i.NfsSKeys != nil {
+ err = errors.New("already initialized")
+ return
+ }
+ l := len(nfsSKeysBytes)
+ if l == 0 {
+ err = errors.New("empty nfsSKeysBytes")
+ return
+ }
+ i.NfsSKeys = make([]any, l)
+ i.NfsPKeysBytes = make([][]byte, l)
+ i.Hash32s = make([][32]byte, l)
+ for j, k := range nfsSKeysBytes {
+ if len(k) == 32 {
+ if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil {
+ return
+ }
+ i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes()
+ i.RelaysLength += 32 + 32
+ } else {
+ if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil {
+ return
+ }
+ i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes()
+ i.RelaysLength += 1088 + 32
+ }
+ i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j])
+ }
+ i.RelaysLength -= 32
+ i.XorMode = xorMode
+ if seconds > 0 {
+ i.Seconds = seconds
+ i.Sessions = make(map[[16]byte]*ServerSession)
+ go func() {
+ for {
+ time.Sleep(time.Minute)
+ i.RWLock.Lock()
+ if i.Closed {
+ i.RWLock.Unlock()
+ return
+ }
+ now := time.Now()
+ for ticket, session := range i.Sessions {
+ if now.After(session.Expire) {
+ delete(i.Sessions, ticket)
+ }
+ }
+ 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) (*CommonConn, error) {
+ if i.NfsSKeys == nil {
+ return nil, errors.New("uninitialized")
+ }
+ c := NewCommonConn(conn)
+
+ ivAndRelays := make([]byte, 16+i.RelaysLength)
+ if _, err := io.ReadFull(conn, ivAndRelays); err != nil {
+ return nil, err
+ }
+ iv := ivAndRelays[:16]
+ relays := ivAndRelays[16:]
+ var nfsKey []byte
+ var lastCTR cipher.Stream
+ for j, k := range i.NfsSKeys {
+ if lastCTR != nil {
+ lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay
+ }
+ var index = 32
+ if _, ok := k.(*mlkem.DecapsulationKey768); ok {
+ index = 1088
+ }
+ if i.XorMode > 0 {
+ NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator, because we have PSK :)
+ }
+ if k, ok := k.(*ecdh.PrivateKey); ok {
+ publicKey, err := ecdh.X25519().NewPublicKey(relays[:index])
+ if err != nil {
+ return nil, err
+ }
+ nfsKey, err = k.ECDH(publicKey)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if k, ok := k.(*mlkem.DecapsulationKey768); ok {
+ var err error
+ nfsKey, err = k.Decapsulate(relays[:index])
+ if err != nil {
+ return nil, err
+ }
+ }
+ if j == len(i.NfsSKeys)-1 {
+ break
+ }
+ relays = relays[index:]
+ lastCTR = NewCTR(nfsKey, iv)
+ lastCTR.XORKeyStream(relays, relays[:32])
+ if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) {
+ return nil, errors.New("unexpected hash32: ", fmt.Sprintf("%v", relays[:32]))
+ }
+ relays = relays[32:]
+ }
+ nfsGCM := NewGCM(iv, nfsKey)
+
+ encryptedLength := make([]byte, 18)
+ if _, err := io.ReadFull(conn, encryptedLength); err != nil {
+ return nil, err
+ }
+ if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
+ return nil, err
+ }
+ length := DecodeLength(encryptedLength[:2])
+
+ if length == 32 {
+ if i.Seconds == 0 {
+ return nil, errors.New("0-RTT is not allowed")
+ }
+ encryptedTicket := make([]byte, 32)
+ if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
+ return nil, err
+ }
+ ticket, err := nfsGCM.Open(nil, nil, encryptedTicket, nil)
+ if err != nil {
+ return nil, err
+ }
+ i.RWLock.RLock()
+ 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
+ var err error
+ for err == nil {
+ rand.Read(noises)
+ _, err = DecodeHeader(noises)
+ }
+ conn.Write(noises) // make client do new handshake
+ return nil, errors.New("expired ticket")
+ }
+ if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also
+ return nil, errors.New("replay detected")
+ }
+ c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request)
+ c.PreWrite = make([]byte, 16)
+ rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub")
+ c.GCM = NewGCM(c.PreWrite, c.UnitedKey)
+ c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client)
+ if i.XorMode == 2 {
+ c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client
+ }
+ return c, nil
+ }
+
+ if length < 1184+32+16 { // client may send more public keys in the future's version
+ return nil, errors.New("too short length")
+ }
+ encryptedPfsPublicKey := make([]byte, length)
+ if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
+ return nil, err
+ }
+ if _, err := nfsGCM.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil {
+ return nil, err
+ }
+ mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184])
+ if err != nil {
+ return nil, err
+ }
+ mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate()
+ peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32])
+ if err != nil {
+ return nil, err
+ }
+ x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
+ x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
+ if err != nil {
+ return nil, err
+ }
+ pfsKey := make([]byte, 32+32) // no more capacity
+ copy(pfsKey, mlkem768Key)
+ copy(pfsKey[32:], x25519Key)
+ pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...)
+ c.UnitedKey = append(pfsKey, nfsKey...)
+ c.GCM = NewGCM(pfsPublicKey, c.UnitedKey)
+ c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1184+32], c.UnitedKey)
+ ticket := make([]byte, 16)
+ rand.Read(ticket)
+ copy(ticket, EncodeLength(int(i.Seconds*4/5)))
+
+ pfsKeyExchangeLength := 1088 + 32 + 16
+ encryptedTicketLength := 32
+ paddingLength := int(crypto.RandBetween(100, 1000))
+ serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength)
+ nfsGCM.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil)
+ c.GCM.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket, nil)
+ padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:]
+ c.GCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
+ c.GCM.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,
+ }
+ i.RWLock.Unlock()
+ }
+
+ // important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern
+ if _, err := io.ReadFull(conn, encryptedLength); err != nil {
+ return nil, err
+ }
+ if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
+ return nil, err
+ }
+ encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2]))
+ if _, err := io.ReadFull(conn, encryptedPadding); err != nil {
+ return nil, err
+ }
+ if _, err := nfsGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil {
+ return nil, err
+ }
+
+ if i.XorMode == 2 {
+ c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0)
+ }
+ return c, nil
+}
diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go
new file mode 100644
index 000000000000..e435cb5ce880
--- /dev/null
+++ b/proxy/vless/encryption/xor.go
@@ -0,0 +1,93 @@
+package encryption
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "net"
+
+ "lukechampine.com/blake3"
+)
+
+func NewCTR(key, iv []byte) cipher.Stream {
+ k := make([]byte, 32)
+ blake3.DeriveKey(k, "VLESS", key) // avoids using key directly
+ block, _ := aes.NewCipher(k)
+ return cipher.NewCTR(block, iv)
+ //chacha20.NewUnauthenticatedCipher()
+}
+
+type XorConn struct {
+ net.Conn
+ CTR cipher.Stream
+ PeerCTR cipher.Stream
+ OutSkip int
+ OutHeader []byte
+ InSkip int
+ InHeader []byte
+}
+
+func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn {
+ return &XorConn{
+ Conn: conn,
+ CTR: ctr,
+ PeerCTR: peerCTR,
+ OutSkip: outSkip,
+ OutHeader: make([]byte, 0, 5), // important
+ InSkip: inSkip,
+ InHeader: make([]byte, 0, 5), // important
+ }
+}
+
+func (c *XorConn) Write(b []byte) (int, error) {
+ if len(b) == 0 {
+ return 0, nil
+ }
+ for p := b; ; {
+ if len(p) <= c.OutSkip {
+ c.OutSkip -= len(p)
+ break
+ }
+ p = p[c.OutSkip:]
+ c.OutSkip = 0
+ need := 5 - len(c.OutHeader)
+ if len(p) < need {
+ c.OutHeader = append(c.OutHeader, p...)
+ c.CTR.XORKeyStream(p, p)
+ break
+ }
+ c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...))
+ c.OutHeader = c.OutHeader[:0]
+ c.CTR.XORKeyStream(p[:need], p[:need])
+ p = p[need:]
+ }
+ if _, err := c.Conn.Write(b); err != nil {
+ return 0, err
+ }
+ return len(b), nil
+}
+
+func (c *XorConn) Read(b []byte) (int, error) {
+ if len(b) == 0 {
+ return 0, nil
+ }
+ n, err := c.Conn.Read(b)
+ for p := b[:n]; ; {
+ if len(p) <= c.InSkip {
+ c.InSkip -= len(p)
+ break
+ }
+ p = p[c.InSkip:]
+ c.InSkip = 0
+ need := 5 - len(c.InHeader)
+ if len(p) < need {
+ c.PeerCTR.XORKeyStream(p, p)
+ c.InHeader = append(c.InHeader, p...)
+ break
+ }
+ c.PeerCTR.XORKeyStream(p[:need], p[:need])
+ c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...))
+ c.InHeader = c.InHeader[:0]
+ p = p[need:]
+ }
+ return n, err
+}
diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go
index 907a3f7f3979..e3192cf8ec73 100644
--- a/proxy/vless/inbound/config.pb.go
+++ b/proxy/vless/inbound/config.pb.go
@@ -111,11 +111,11 @@ type Config struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"`
- // Decryption settings. Only applies to server side, and only accepts "none"
- // for now.
- Decryption string `protobuf:"bytes,2,opt,name=decryption,proto3" json:"decryption,omitempty"`
- Fallbacks []*Fallback `protobuf:"bytes,3,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"`
+ Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"`
+ 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"`
+ Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"`
}
func (x *Config) Reset() {
@@ -155,6 +155,13 @@ func (x *Config) GetClients() []*protocol.User {
return nil
}
+func (x *Config) GetFallbacks() []*Fallback {
+ if x != nil {
+ return x.Fallbacks
+ }
+ return nil
+}
+
func (x *Config) GetDecryption() string {
if x != nil {
return x.Decryption
@@ -162,11 +169,18 @@ func (x *Config) GetDecryption() string {
return ""
}
-func (x *Config) GetFallbacks() []*Fallback {
+func (x *Config) GetXorMode() uint32 {
if x != nil {
- return x.Fallbacks
+ return x.XorMode
}
- return nil
+ return 0
+}
+
+func (x *Config) GetSeconds() uint32 {
+ if x != nil {
+ return x.Seconds
+ }
+ return 0
}
var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor
@@ -185,25 +199,28 @@ 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, 0xa0, 0x01,
+ 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xd4, 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,
- 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1e,
- 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40,
- 0x0a, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
+ 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x40,
+ 0x0a, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x22, 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, 0x2e, 0x46, 0x61, 0x6c,
0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 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,
+ 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03,
+ 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, 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,
}
var (
diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto
index 94b5551c54db..e1ebc8d37059 100644
--- a/proxy/vless/inbound/config.proto
+++ b/proxy/vless/inbound/config.proto
@@ -19,8 +19,9 @@ message Fallback {
message Config {
repeated xray.common.protocol.User clients = 1;
- // Decryption settings. Only applies to server side, and only accepts "none"
- // for now.
- string decryption = 2;
- repeated Fallback fallbacks = 3;
+ repeated Fallback fallbacks = 2;
+
+ string decryption = 3;
+ uint32 xorMode = 4;
+ uint32 seconds = 5;
}
diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go
index dfa8d470aa1f..ca5176b928af 100644
--- a/proxy/vless/inbound/inbound.go
+++ b/proxy/vless/inbound/inbound.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
gotls "crypto/tls"
+ "encoding/base64"
"io"
"reflect"
"strconv"
@@ -29,6 +30,7 @@ import (
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/encoding"
+ "github.com/xtls/xray-core/proxy/vless/encryption"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
@@ -67,6 +69,7 @@ type Handler struct {
policyManager policy.Manager
validator vless.Validator
dns dns.Client
+ decryption *encryption.ServerInstance
fallbacks map[string]map[string]map[string]*Fallback // or nil
// regexps map[string]*regexp.Regexp // or nil
}
@@ -81,6 +84,19 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val
validator: validator,
}
+ if config.Decryption != "" && config.Decryption != "none" {
+ s := strings.Split(config.Decryption, ".")
+ var nfsSKeysBytes [][]byte
+ for _, r := range s {
+ b, _ := base64.RawURLEncoding.DecodeString(r)
+ nfsSKeysBytes = append(nfsSKeysBytes, b)
+ }
+ handler.decryption = &encryption.ServerInstance{}
+ if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.Seconds); err != nil {
+ return nil, errors.New("failed to use decryption").Base(err).AtError()
+ }
+ }
+
if config.Fallbacks != nil {
handler.fallbacks = make(map[string]map[string]map[string]*Fallback)
// handler.regexps = make(map[string]*regexp.Regexp)
@@ -159,6 +175,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))
}
@@ -199,6 +218,14 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
iConn = statConn.Connection
}
+ if h.decryption != nil {
+ var err error
+ connection, err = h.decryption.Handshake(connection)
+ if err != nil {
+ return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo()
+ }
+ }
+
sessionPolicy := h.policyManager.ForLevel(0)
if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
return errors.New("unable to set read deadline").Base(err).AtWarning()
@@ -464,6 +491,7 @@ 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 {
@@ -476,6 +504,13 @@ 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 {
@@ -544,7 +579,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, input, rawInput, trafficState, nil, true, ctx1)
+ err = encoding.XtlsRead(clientReader, serverWriter, timer, connection, peerCache, input, rawInput, trafficState, nil, true, ctx1)
} else {
// from clientReader.ReadMultiBuffer to serverWriter.WriteMultiBuffer
err = buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer))
diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go
index e1a727eb120a..ee1b6dfb4caa 100644
--- a/proxy/vless/outbound/outbound.go
+++ b/proxy/vless/outbound/outbound.go
@@ -4,7 +4,9 @@ import (
"bytes"
"context"
gotls "crypto/tls"
+ "encoding/base64"
"reflect"
+ "strings"
"time"
"unsafe"
@@ -24,6 +26,7 @@ import (
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/vless"
"github.com/xtls/xray-core/proxy/vless/encoding"
+ "github.com/xtls/xray-core/proxy/vless/encryption"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
@@ -43,6 +46,7 @@ type Handler struct {
serverPicker protocol.ServerPicker
policyManager policy.Manager
cone bool
+ encryption *encryption.ClientInstance
}
// New creates a new VLess outbound handler.
@@ -64,6 +68,20 @@ func New(ctx context.Context, config *Config) (*Handler, error) {
cone: ctx.Value("cone").(bool),
}
+ a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount)
+ if a.Encryption != "" && a.Encryption != "none" {
+ s := strings.Split(a.Encryption, ".")
+ var nfsPKeysBytes [][]byte
+ for _, r := range s {
+ b, _ := base64.RawURLEncoding.DecodeString(r)
+ nfsPKeysBytes = append(nfsPKeysBytes, b)
+ }
+ handler.encryption = &encryption.ClientInstance{}
+ if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds); err != nil {
+ return nil, errors.New("failed to use encryption").Base(err).AtError()
+ }
+ }
+
return handler, nil
}
@@ -98,6 +116,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
target := ob.Target
errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination().NetAddr())
+ if h.encryption != nil {
+ var err error
+ conn, err = h.encryption.Handshake(conn)
+ if err != nil {
+ return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo()
+ }
+ }
+
command := protocol.RequestCommandTCP
if target.Network == net.Network_UDP {
command = protocol.RequestCommandUDP
@@ -120,6 +146,7 @@ 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
@@ -138,6 +165,13 @@ 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 {
@@ -272,7 +306,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, input, rawInput, trafficState, ob, false, ctx)
+ err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, peerCache, input, rawInput, trafficState, ob, false, ctx)
} else {
// from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBuffer
err = buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer))
From 03174944f4a18f9961acb4bdb0850f9492b274b2 Mon Sep 17 00:00:00 2001
From: RPRX <63339210+RPRX@users.noreply.github.com>
Date: Thu, 28 Aug 2025 05:01:35 +0000
Subject: [PATCH 02/10] README.md: Update Donation & NFTs
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1
VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless
XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 511efe20ffce..c600de24f980 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,10 @@
[
](https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1)
- **ETH/USDT/USDC: `0xDc3Fe44F0f25D13CACb1C4896CD0D321df3146Ee`**
+- **Project X NFT: https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1**
+- **VLESS NFT: https://opensea.io/collection/vless**
- **REALITY NFT: https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2**
-- **Related links: https://opensea.io/collection/xtls, [Announcement of NFTs by Project X](https://github.com/XTLS/Xray-core/discussions/3633), [XHTTP: Beyond REALITY](https://github.com/XTLS/Xray-core/discussions/4113)**
+- **Related links: [VLESS Post-Quantum Encryption](https://github.com/XTLS/Xray-core/pull/5067), [XHTTP: Beyond REALITY](https://github.com/XTLS/Xray-core/discussions/4113), [Announcement of NFTs by Project X](https://github.com/XTLS/Xray-core/discussions/3633)**
## License
From 200588196bb608c79563796bfb00f5a774bfb9a8 Mon Sep 17 00:00:00 2001
From: RPRX <63339210+RPRX@users.noreply.github.com>
Date: Thu, 28 Aug 2025 05:09:53 +0000
Subject: [PATCH 03/10] Update github.com/xtls/reality to 20250828044527
https://github.com/XTLS/REALITY/commit/046fad5ab64f9bda1a8fbc65647514c5a805f843
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index c5e8d2e2a871..4e50e20ded68 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
github.com/stretchr/testify v1.11.0
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e
github.com/vishvananda/netlink v1.3.1
- github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7
+ github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
diff --git a/go.sum b/go.sum
index f6c95e350e5b..a33479482420 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
-github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7 h1:Ript0vN+nSO33+Vj4n0mgNY5M+oOxFQJdrJ1VnwTBO0=
-github.com/xtls/reality v0.0.0-20250725142056-5b52a03d4fb7/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
+github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f h1:o1Kryl9qEYYzNep9RId9DM1kBn8tBrcK5UJnti/l0NI=
+github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
From bf63cebea74ab18e9c464ae08832fd646a55def8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 28 Aug 2025 05:12:48 +0000
Subject: [PATCH 04/10] Bump github.com/stretchr/testify from 1.11.0 to 1.11.1
(#5068)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.0...v1.11.1)
---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
dependency-version: 1.11.1
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 4e50e20ded68..13a0bf8c771a 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771
- github.com/stretchr/testify v1.11.0
+ github.com/stretchr/testify v1.11.1
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f
diff --git a/go.sum b/go.sum
index a33479482420..268254b7835c 100644
--- a/go.sum
+++ b/go.sum
@@ -67,8 +67,8 @@ github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
-github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
From 9ccf82ac56a8837bd2981b196b2e1058d8249293 Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Fri, 22 Aug 2025 23:31:58 +0330
Subject: [PATCH 05/10] UDP dispatcher: simplified and optimized
---
transport/internet/udp/dispatcher.go | 48 +++++++++++++++-------------
1 file changed, 25 insertions(+), 23 deletions(-)
diff --git a/transport/internet/udp/dispatcher.go b/transport/internet/udp/dispatcher.go
index 22db4244b495..9f5144eceb04 100644
--- a/transport/internet/udp/dispatcher.go
+++ b/transport/internet/udp/dispatcher.go
@@ -24,6 +24,15 @@ type connEntry struct {
link *transport.Link
timer signal.ActivityUpdater
cancel context.CancelFunc
+ closed bool
+}
+
+func (c *connEntry) Close() error {
+ c.closed = true
+ c.cancel()
+ common.Interrupt(c.link.Reader)
+ common.Close(c.link.Writer)
+ return nil
}
type Dispatcher struct {
@@ -44,13 +53,8 @@ func NewDispatcher(dispatcher routing.Dispatcher, callback ResponseCallback) *Di
func (v *Dispatcher) RemoveRay() {
v.Lock()
defer v.Unlock()
- v.removeRay()
-}
-
-func (v *Dispatcher) removeRay() {
if v.conn != nil {
- common.Interrupt(v.conn.link.Reader)
- common.Close(v.conn.link.Writer)
+ v.conn.Close()
v.conn = nil
}
}
@@ -60,34 +64,32 @@ func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*
defer v.Unlock()
if v.conn != nil {
- return v.conn, nil
+ if v.conn.closed {
+ v.conn = nil
+ } else {
+ return v.conn, nil
+ }
}
errors.LogInfo(ctx, "establishing new connection for ", dest)
ctx, cancel := context.WithCancel(ctx)
- entry := &connEntry{}
- removeRay := func() {
- v.Lock()
- defer v.Unlock()
- // sometimes the entry is already removed by others, don't close again
- if entry == v.conn {
- cancel()
- v.removeRay()
- }
- }
- timer := signal.CancelAfterInactivity(ctx, removeRay, time.Minute)
link, err := v.dispatcher.Dispatch(ctx, dest)
if err != nil {
+ cancel()
return nil, errors.New("failed to dispatch request to ", dest).Base(err)
}
- *entry = connEntry{
+ entry := &connEntry{
link: link,
- timer: timer,
- cancel: removeRay,
+ cancel: cancel,
}
+ entryClose := func() {
+ entry.Close()
+ }
+
+ entry.timer = signal.CancelAfterInactivity(ctx, entryClose, time.Minute)
v.conn = entry
go handleInput(ctx, entry, dest, v.callback, v.callClose)
return entry, nil
@@ -106,7 +108,7 @@ func (v *Dispatcher) Dispatch(ctx context.Context, destination net.Destination,
if outputStream != nil {
if err := outputStream.WriteMultiBuffer(buf.MultiBuffer{payload}); err != nil {
errors.LogInfoInner(ctx, err, "failed to write first UDP payload")
- conn.cancel()
+ conn.Close()
return
}
}
@@ -114,7 +116,7 @@ func (v *Dispatcher) Dispatch(ctx context.Context, destination net.Destination,
func handleInput(ctx context.Context, conn *connEntry, dest net.Destination, callback ResponseCallback, callClose func() error) {
defer func() {
- conn.cancel()
+ conn.Close()
if callClose != nil {
callClose()
}
From ded981d625ccd05e42b2162e9ec0b12ff45060f7 Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Sat, 23 Aug 2025 02:26:04 +0330
Subject: [PATCH 06/10] fix
---
app/proxyman/inbound/worker.go | 4 ++--
proxy/freedom/freedom.go | 4 ++--
transport/internet/udp/dispatcher.go | 22 ++++++++++++++++------
3 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go
index 40c8bc157e5f..ebe4bc89e7f1 100644
--- a/app/proxyman/inbound/worker.go
+++ b/app/proxyman/inbound/worker.go
@@ -382,7 +382,7 @@ func (w *udpWorker) clean() error {
}
for addr, conn := range w.activeConn {
- if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 2*60 {
+ if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 60 {
if !conn.inactive {
conn.setInactive()
delete(w.activeConn, addr)
@@ -409,7 +409,7 @@ func (w *udpWorker) Start() error {
w.cone = w.ctx.Value("cone").(bool)
w.checker = &task.Periodic{
- Interval: time.Minute,
+ Interval: 30 * time.Second,
Execute: w.clean,
}
diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go
index 0e9937e3c7a5..f8d64812f02a 100644
--- a/proxy/freedom/freedom.go
+++ b/proxy/freedom/freedom.go
@@ -73,7 +73,7 @@ func isValidAddress(addr *net.IPOrDomain) bool {
}
a := addr.AsAddress()
- return a != net.AnyIP
+ return a != net.AnyIP && a != net.AnyIPv6
}
// Process implements proxy.Outbound.
@@ -418,7 +418,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
}
}
}
- destAddr, _ := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
+ destAddr := b.UDP.RawNetAddr()
if destAddr == nil {
b.Release()
continue
diff --git a/transport/internet/udp/dispatcher.go b/transport/internet/udp/dispatcher.go
index 9f5144eceb04..d383e316a993 100644
--- a/transport/internet/udp/dispatcher.go
+++ b/transport/internet/udp/dispatcher.go
@@ -22,17 +22,24 @@ type ResponseCallback func(ctx context.Context, packet *udp.Packet)
type connEntry struct {
link *transport.Link
- timer signal.ActivityUpdater
+ timer *signal.ActivityTimer
cancel context.CancelFunc
closed bool
}
func (c *connEntry) Close() error {
+ c.timer.SetTimeout(0)
+ return nil
+}
+
+func (c *connEntry) terminate() {
+ if c.closed {
+ panic("terminate called more than once")
+ }
c.closed = true
c.cancel()
common.Interrupt(c.link.Reader)
common.Close(c.link.Writer)
- return nil
}
type Dispatcher struct {
@@ -41,6 +48,7 @@ type Dispatcher struct {
dispatcher routing.Dispatcher
callback ResponseCallback
callClose func() error
+ closed bool
}
func NewDispatcher(dispatcher routing.Dispatcher, callback ResponseCallback) *Dispatcher {
@@ -53,6 +61,7 @@ func NewDispatcher(dispatcher routing.Dispatcher, callback ResponseCallback) *Di
func (v *Dispatcher) RemoveRay() {
v.Lock()
defer v.Unlock()
+ v.closed = true
if v.conn != nil {
v.conn.Close()
v.conn = nil
@@ -63,6 +72,10 @@ func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*
v.Lock()
defer v.Unlock()
+ if v.closed {
+ return nil, errors.New("dispatcher is closed")
+ }
+
if v.conn != nil {
if v.conn.closed {
v.conn = nil
@@ -85,11 +98,8 @@ func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*
link: link,
cancel: cancel,
}
- entryClose := func() {
- entry.Close()
- }
- entry.timer = signal.CancelAfterInactivity(ctx, entryClose, time.Minute)
+ entry.timer = signal.CancelAfterInactivity(ctx, entry.terminate, 30*time.Second) // The UDP timeout is set to 30 seconds in most NAT configurations
v.conn = entry
go handleInput(ctx, entry, dest, v.callback, v.callClose)
return entry, nil
From 951de56e0846a6cac9efe799397cbb6ffac501a5 Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Sat, 23 Aug 2025 06:05:28 +0330
Subject: [PATCH 07/10] fix memory leak
---
app/dns/nameserver_udp.go | 4 +++-
proxy/shadowsocks/server.go | 6 +++---
proxy/socks/server.go | 3 ++-
proxy/trojan/protocol.go | 2 ++
transport/internet/udp/dispatcher.go | 2 +-
5 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go
index 3c25e612570c..066e6868fef6 100644
--- a/app/dns/nameserver_udp.go
+++ b/app/dns/nameserver_udp.go
@@ -90,7 +90,9 @@ func (s *ClassicNameServer) RequestsCleanup() error {
// HandleResponse handles udp response packet from remote DNS server.
func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) {
- ipRec, err := parseResponse(packet.Payload.Bytes())
+ payload := packet.Payload
+ ipRec, err := parseResponse(payload.Bytes())
+ payload.Release()
if err != nil {
errors.LogError(ctx, s.Name(), " fail to parse responded DNS udp")
return
diff --git a/proxy/shadowsocks/server.go b/proxy/shadowsocks/server.go
index ec0220846424..360ea38c8d53 100644
--- a/proxy/shadowsocks/server.go
+++ b/proxy/shadowsocks/server.go
@@ -104,12 +104,12 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) {
request := protocol.RequestHeaderFromContext(ctx)
+ payload := packet.Payload
if request == nil {
+ payload.Release()
return
}
- payload := packet.Payload
-
if payload.UDP != nil {
request = &protocol.RequestHeader{
User: request.User,
@@ -124,9 +124,9 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
errors.LogWarningInner(ctx, err, "failed to encode UDP packet")
return
}
- defer data.Release()
conn.Write(data.Bytes())
+ data.Release()
})
defer udpServer.RemoveRay()
diff --git a/proxy/socks/server.go b/proxy/socks/server.go
index 13d988512941..166deaa3856e 100644
--- a/proxy/socks/server.go
+++ b/proxy/socks/server.go
@@ -196,6 +196,7 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
request := protocol.RequestHeaderFromContext(ctx)
if request == nil {
+ payload.Release()
return
}
@@ -214,9 +215,9 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
errors.LogWarningInner(ctx, err, "failed to write UDP response")
return
}
- defer udpMessage.Release()
conn.Write(udpMessage.Bytes())
+ udpMessage.Release()
})
defer udpServer.RemoveRay()
diff --git a/proxy/trojan/protocol.go b/proxy/trojan/protocol.go
index 96a166381ce4..889ccc5ce4c2 100644
--- a/proxy/trojan/protocol.go
+++ b/proxy/trojan/protocol.go
@@ -113,9 +113,11 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
target = b.UDP
}
if _, err := w.writePacket(b.Bytes(), *target); err != nil {
+ b.Release()
buf.ReleaseMulti(mb)
return err
}
+ b.Release()
}
return nil
}
diff --git a/transport/internet/udp/dispatcher.go b/transport/internet/udp/dispatcher.go
index d383e316a993..f4507fcdd8e6 100644
--- a/transport/internet/udp/dispatcher.go
+++ b/transport/internet/udp/dispatcher.go
@@ -39,7 +39,7 @@ func (c *connEntry) terminate() {
c.closed = true
c.cancel()
common.Interrupt(c.link.Reader)
- common.Close(c.link.Writer)
+ common.Interrupt(c.link.Writer)
}
type Dispatcher struct {
From 944138e1c04023b55f43ef783044520b6cf132df Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Sun, 24 Aug 2025 02:26:24 +0330
Subject: [PATCH 08/10] more fix
---
app/proxyman/outbound/handler.go | 10 ++++++++--
common/mux/client.go | 19 ++++++++++---------
common/mux/server.go | 2 +-
proxy/proxy.go | 3 +++
transport/pipe/impl.go | 13 ++++++++-----
5 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go
index d0e670c501f7..ef5eed0ebc79 100644
--- a/app/proxyman/outbound/handler.go
+++ b/app/proxyman/outbound/handler.go
@@ -239,8 +239,10 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
}
out:
err := h.proxy.Process(ctx, link, h)
+ var errC error
if err != nil {
- if goerrors.Is(err, io.EOF) || goerrors.Is(err, io.ErrClosedPipe) || goerrors.Is(err, context.Canceled) {
+ errC = errors.Cause(err)
+ if goerrors.Is(errC, io.EOF) || goerrors.Is(errC, io.ErrClosedPipe) || goerrors.Is(errC, context.Canceled) {
err = nil
}
}
@@ -251,7 +253,11 @@ out:
errors.LogInfo(ctx, err.Error())
common.Interrupt(link.Writer)
} else {
- common.Close(link.Writer)
+ if errC != nil && goerrors.Is(errC, io.ErrClosedPipe) {
+ common.Interrupt(link.Writer)
+ } else {
+ common.Close(link.Writer)
+ }
}
common.Interrupt(link.Reader)
}
diff --git a/common/mux/client.go b/common/mux/client.go
index 6987f762f847..32746f3961f9 100644
--- a/common/mux/client.go
+++ b/common/mux/client.go
@@ -2,6 +2,7 @@ package mux
import (
"context"
+ goerrors "errors"
"io"
"sync"
"time"
@@ -154,8 +155,11 @@ func (f *DialingWorkerFactory) Create() (*ClientWorker, error) {
ctx := session.ContextWithOutbounds(context.Background(), outbounds)
ctx, cancel := context.WithCancel(ctx)
- if err := p.Process(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter}, d); err != nil {
- errors.LogInfoInner(ctx, err, "failed to handler mux client connection")
+ if errP := p.Process(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter}, d); errP != nil {
+ errC := errors.Cause(errP)
+ if !(goerrors.Is(errC, io.EOF) || goerrors.Is(errC, io.ErrClosedPipe) || goerrors.Is(errC, context.Canceled)) {
+ errors.LogInfoInner(ctx, errP, "failed to handler mux client connection")
+ }
}
common.Must(c.Close())
cancel()
@@ -222,7 +226,7 @@ func (m *ClientWorker) monitor() {
select {
case <-m.done.Wait():
m.sessionManager.Close()
- common.Close(m.link.Writer)
+ common.Interrupt(m.link.Writer)
common.Interrupt(m.link.Reader)
return
case <-m.timer.C:
@@ -247,7 +251,7 @@ func writeFirstPayload(reader buf.Reader, writer *Writer) error {
return nil
}
-func fetchInput(ctx context.Context, s *Session, output buf.Writer) {
+func fetchInput(ctx context.Context, s *Session, output buf.Writer, timer *time.Ticker) {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
transferType := protocol.TransferTypeStream
@@ -258,6 +262,7 @@ func fetchInput(ctx context.Context, s *Session, output buf.Writer) {
writer := NewWriter(s.ID, ob.Target, output, transferType, xudp.GetGlobalID(ctx))
defer s.Close(false)
defer writer.Close()
+ defer timer.Reset(time.Second * 16)
errors.LogInfo(ctx, "dispatching request to ", ob.Target)
if err := writeFirstPayload(s.input, writer); err != nil {
@@ -307,11 +312,7 @@ func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool
}
s.input = link.Reader
s.output = link.Writer
- if _, ok := link.Reader.(*pipe.Reader); ok {
- go fetchInput(ctx, s, m.link.Writer)
- } else {
- fetchInput(ctx, s, m.link.Writer)
- }
+ go fetchInput(ctx, s, m.link.Writer, m.timer)
return true
}
diff --git a/common/mux/server.go b/common/mux/server.go
index 0a632e819cbb..12e4a68f1a1f 100644
--- a/common/mux/server.go
+++ b/common/mux/server.go
@@ -318,8 +318,8 @@ func (w *ServerWorker) run(ctx context.Context) {
reader := &buf.BufferedReader{Reader: w.link.Reader}
defer w.sessionManager.Close()
- defer common.Close(w.link.Writer)
defer common.Interrupt(w.link.Reader)
+ defer common.Interrupt(w.link.Writer)
for {
select {
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 049d9fbdbb74..edfa63d05a56 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -636,6 +636,9 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net
}
}
if err != nil {
+ if errors.Cause(err) == io.EOF {
+ return nil
+ }
return err
}
}
diff --git a/transport/pipe/impl.go b/transport/pipe/impl.go
index 4a30dbbb91b3..e5d678272ab9 100644
--- a/transport/pipe/impl.go
+++ b/transport/pipe/impl.go
@@ -200,16 +200,19 @@ func (p *pipe) Interrupt() {
p.Lock()
defer p.Unlock()
+ if !p.data.IsEmpty() {
+ buf.ReleaseMulti(p.data)
+ p.data = nil
+ if p.state == closed {
+ p.state = errord
+ }
+ }
+
if p.state == closed || p.state == errord {
return
}
p.state = errord
- if !p.data.IsEmpty() {
- buf.ReleaseMulti(p.data)
- p.data = nil
- }
-
common.Must(p.done.Close())
}
From e23f55c8581f50d8a3b53b9cbe71c241b80e1695 Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:07:12 +0330
Subject: [PATCH 09/10] fix udp_nameserver not set b.UDP
---
app/dns/nameserver_udp.go | 4 ++++
app/proxyman/inbound/worker.go | 4 ++--
transport/internet/udp/dispatcher.go | 2 +-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go
index 066e6868fef6..e29f6e2441f2 100644
--- a/app/dns/nameserver_udp.go
+++ b/app/dns/nameserver_udp.go
@@ -127,6 +127,8 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot
newReq.msg = &newMsg
s.addPendingRequest(&newReq)
b, _ := dns.PackMessage(newReq.msg)
+ copyDest := net.UDPDestination(s.address.Address, s.address.Port)
+ b.UDP = ©Dest
s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b)
return
}
@@ -160,6 +162,8 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, domai
}
s.addPendingRequest(udpReq)
b, _ := dns.PackMessage(req.msg)
+ copyDest := net.UDPDestination(s.address.Address, s.address.Port)
+ b.UDP = ©Dest
s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b)
}
}
diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go
index ebe4bc89e7f1..40c8bc157e5f 100644
--- a/app/proxyman/inbound/worker.go
+++ b/app/proxyman/inbound/worker.go
@@ -382,7 +382,7 @@ func (w *udpWorker) clean() error {
}
for addr, conn := range w.activeConn {
- if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 60 {
+ if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 2*60 {
if !conn.inactive {
conn.setInactive()
delete(w.activeConn, addr)
@@ -409,7 +409,7 @@ func (w *udpWorker) Start() error {
w.cone = w.ctx.Value("cone").(bool)
w.checker = &task.Periodic{
- Interval: 30 * time.Second,
+ Interval: time.Minute,
Execute: w.clean,
}
diff --git a/transport/internet/udp/dispatcher.go b/transport/internet/udp/dispatcher.go
index f4507fcdd8e6..963ce662a6a8 100644
--- a/transport/internet/udp/dispatcher.go
+++ b/transport/internet/udp/dispatcher.go
@@ -99,7 +99,7 @@ func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*
cancel: cancel,
}
- entry.timer = signal.CancelAfterInactivity(ctx, entry.terminate, 30*time.Second) // The UDP timeout is set to 30 seconds in most NAT configurations
+ entry.timer = signal.CancelAfterInactivity(ctx, entry.terminate, time.Minute)
v.conn = entry
go handleInput(ctx, entry, dest, v.callback, v.callClose)
return entry, nil
From 621232ab876b038480ab719f79d0d7b65dc03d7a Mon Sep 17 00:00:00 2001
From: patterniha <71074308+patterniha@users.noreply.github.com>
Date: Fri, 29 Aug 2025 16:22:35 +0330
Subject: [PATCH 10/10] Update client.go
---
common/mux/client.go | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/common/mux/client.go b/common/mux/client.go
index 32746f3961f9..e94fd3ad03d7 100644
--- a/common/mux/client.go
+++ b/common/mux/client.go
@@ -312,7 +312,11 @@ func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool
}
s.input = link.Reader
s.output = link.Writer
- go fetchInput(ctx, s, m.link.Writer, m.timer)
+ if _, ok := link.Reader.(*pipe.Reader); ok {
+ go fetchInput(ctx, s, m.link.Writer, m.timer)
+ } else {
+ fetchInput(ctx, s, m.link.Writer, m.timer)
+ }
return true
}