From f61c14e9c63dc41a8a09135db3aea337974f3f37 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:50:18 +0000 Subject: [PATCH 01/25] VLESS protocol: Add lightweight Post-Quantum ML-KEM-768-based PFS 1-RTT / anti-replay 0-RTT AEAD encryption https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3163335040 --- common/protocol/headers.go | 14 +- infra/conf/vless.go | 82 ++++++++-- main/commands/all/commands.go | 1 + main/commands/all/mldsa65.go | 14 +- main/commands/all/mlkem768.go | 47 ++++++ main/commands/all/uuid.go | 4 +- main/commands/all/wg.go | 4 +- main/commands/all/x25519.go | 4 +- proxy/vless/account.go | 5 +- proxy/vless/account.pb.go | 29 ++-- proxy/vless/account.proto | 3 +- proxy/vless/encryption/client.go | 228 ++++++++++++++++++++++++++++ proxy/vless/encryption/common.go | 55 +++++++ proxy/vless/encryption/server.go | 251 +++++++++++++++++++++++++++++++ proxy/vless/inbound/config.pb.go | 49 +++--- proxy/vless/inbound/config.proto | 8 +- proxy/vless/inbound/inbound.go | 19 +++ proxy/vless/outbound/outbound.go | 20 +++ 18 files changed, 769 insertions(+), 68 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 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 5d4ace6f9645..ed090facc927 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" @@ -55,12 +56,16 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { account.Id = u.String() switch account.Flow { - case "", vless.XRV: + case "": + case vless.XRV: + if c.Decryption != "none" { + return nil, errors.New(`VLESS clients: "decryption" doesn't support "flow" yet`) + } default: return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } - if account.Encryption != "" { + if len(account.Encryption) > 0 { return nil, errors.New(`VLESS clients: "encryption" should not in inbound settings`) } @@ -68,10 +73,34 @@ 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`) + if !func() bool { + s := strings.Split(c.Decryption, "-mlkem768seed-") + if len(s) != 2 { + return false + } + if s[0] != "1rtt" { + t := strings.TrimSuffix(s[0], "min") + if t == s[0] { + return false + } + i, err := strconv.Atoi(t) + if err != nil { + return false + } + config.Minutes = uint32(i) + } + b, err := base64.RawURLEncoding.DecodeString(s[1]) + if len(b) != 64 || err != nil { + return false + } + config.Decryption = s[1] + return true + }() && c.Decryption != "none" { + if c.Decryption == "" { + return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`) + } + return nil, errors.New(`VLESS settings: unsupported "decryption": ` + c.Decryption) } - config.Decryption = c.Decryption for _, fb := range c.Fallbacks { var i uint16 @@ -143,16 +172,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(), @@ -176,13 +205,42 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { account.Id = u.String() switch account.Flow { - case "", vless.XRV, vless.XRV + "-udp443": + case "": + case vless.XRV, vless.XRV + "-udp443": + if account.Encryption != "none" { + return nil, errors.New(`VLESS users: "encryption" doesn't support "flow" yet`) + } default: 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, "-mlkem768client-") + if len(s) != 2 { + return false + } + if s[0] != "1rtt" { + t := strings.TrimSuffix(s[0], "min") + if t == s[0] { + return false + } + i, err := strconv.Atoi(t) + if err != nil { + return false + } + account.Minutes = uint32(i) + } + b, err := base64.RawURLEncoding.DecodeString(s[1]) + if len(b) != 1184 || err != nil { + return false + } + account.Encryption = s[1] + 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/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..78512bad0a59 --- /dev/null +++ b/main/commands/all/mlkem768.go @@ -0,0 +1,47 @@ +package all + +import ( + "crypto/mlkem" + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/xtls/xray-core/main/commands/base" +) + +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[:]) + pub := key.EncapsulationKey() + fmt.Printf("Seed: %v\nClient: %v", + base64.RawURLEncoding.EncodeToString(seed[:]), + base64.RawURLEncoding.EncodeToString(pub.Bytes())) +} 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..607562b61c81 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 (REALITY)`, Long: ` -Generate key pair for x25519 key exchange. +Generate key pair for X25519 key exchange (REALITY). Random: {{.Exec}} x25519 diff --git a/proxy/vless/account.go b/proxy/vless/account.go index c22cfe16203b..71b2f27405c8 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,6 +18,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? + Minutes: a.Minutes, }, nil } @@ -27,8 +28,9 @@ 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 + Minutes uint32 } // Equals implements protocol.Account.Equals(). @@ -45,5 +47,6 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, + Minutes: a.Minutes, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index fd5d4518468d..be718d29fde9 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -28,9 +28,9 @@ 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"` + Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Account) Reset() { @@ -84,23 +84,32 @@ func (x *Account) GetEncryption() string { return "" } +func (x *Account) GetMinutes() uint32 { + if x != nil { + return x.Minutes + } + 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, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x67, 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, + 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, + 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, + 0x69, 0x6e, 0x75, 0x74, 0x65, 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..1b82a836a60d 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -11,6 +11,7 @@ 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 minutes = 4; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go new file mode 100644 index 000000000000..425b1d007ad6 --- /dev/null +++ b/proxy/vless/encryption/client.go @@ -0,0 +1,228 @@ +package encryption + +import ( + "bytes" + "crypto/cipher" + "crypto/mlkem" + "crypto/rand" + "crypto/sha256" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "golang.org/x/crypto/hkdf" +) + +var ClientCipher byte + +func init() { + if !protocol.HasAESGCMHardwareSupport { + ClientCipher = 1 + } +} + +type ClientInstance struct { + sync.RWMutex + eKeyNfs *mlkem.EncapsulationKey768 + minutes time.Duration + expire time.Time + baseKey []byte + reuse []byte +} + +type ClientConn struct { + net.Conn + instance *ClientInstance + baseKey []byte + reuse []byte + random []byte + aead cipher.AEAD + nonce []byte + peerAead cipher.AEAD + peerNonce []byte + peerCache []byte +} + +func (i *ClientInstance) Init(eKeyNfsData []byte, minutes time.Duration) (err error) { + i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData) + i.minutes = minutes + return +} + +func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { + if i.eKeyNfs == nil { + return nil, errors.New("uninitialized") + } + c := &ClientConn{Conn: conn} + + if i.minutes > 0 { + i.RLock() + if time.Now().Before(i.expire) { + c.instance = i + c.baseKey = i.baseKey + c.reuse = i.reuse + i.RUnlock() + return c, nil + } + i.RUnlock() + } + + nfsKey, encapsulatedNfsKey := i.eKeyNfs.Encapsulate() + seed := make([]byte, 64) + rand.Read(seed) + dKeyPfs, _ := mlkem.NewDecapsulationKey768(seed) + eKeyPfs := dKeyPfs.EncapsulationKey().Bytes() + padding := crypto.RandBetween(100, 1000) + + clientHello := make([]byte, 1088+1184+1+5+padding) + copy(clientHello, encapsulatedNfsKey) + copy(clientHello[1088:], eKeyPfs) + clientHello[2272] = ClientCipher + encodeHeader(clientHello[2273:], int(padding)) + + if _, err := c.Conn.Write(clientHello); err != nil { + return nil, err + } + // we can send more padding if needed + + peerServerHello := make([]byte, 1088+21) + if _, err := io.ReadFull(c.Conn, peerServerHello); err != nil { + return nil, err + } + encapsulatedPfsKey := peerServerHello[:1088] + c.reuse = peerServerHello[1088:] + + pfsKey, err := dKeyPfs.Decapsulate(encapsulatedPfsKey) + if err != nil { + return nil, err + } + c.baseKey = append(nfsKey, pfsKey...) + + authKey := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfs).Read(authKey) + nonce := make([]byte, 12) + VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce, c.reuse, encapsulatedPfsKey) + if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more message + return nil, errors.New("invalid server").AtError() + } + + if i.minutes > 0 { + i.Lock() + i.expire = time.Now().Add(i.minutes) + i.baseKey = c.baseKey + i.reuse = c.reuse + i.Unlock() + } + + return c, nil +} + +func (c *ClientConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + var data []byte + if c.aead == nil { + c.random = make([]byte, 32) + rand.Read(c.random) + key := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, c.random, c.reuse).Read(key) + c.aead = newAead(ClientCipher, key) + c.nonce = make([]byte, 12) + + data = make([]byte, 21+32+5+len(b)+16) + copy(data, c.reuse) + copy(data[21:], c.random) + encodeHeader(data[53:], len(b)+16) + c.aead.Seal(data[:58], c.nonce, b, data[53:58]) + } else { + data = make([]byte, 5+len(b)+16) + encodeHeader(data, len(b)+16) + c.aead.Seal(data[:5], c.nonce, b, data[:5]) + } + increaseNonce(c.nonce) + if _, err := c.Conn.Write(data); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() + if len(b) == 0 { + return 0, nil + } + peerHeader := make([]byte, 5) + if c.peerAead == nil { + if c.instance == nil { + for { + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + peerPadding, _ := decodeHeader(peerHeader) + if peerPadding == 0 { + break + } + if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil { + return 0, err + } + } + } else { + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + } + peerRandom := make([]byte, 32) + copy(peerRandom, peerHeader) + if _, err := io.ReadFull(c.Conn, peerRandom[5:]); err != nil { + return 0, err + } + if c.random == nil { + return 0, errors.New("can not Read() first") + } + peerKey := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, peerRandom, c.random).Read(peerKey) + c.peerAead = newAead(ClientCipher, peerKey) + c.peerNonce = make([]byte, 12) + } + if len(c.peerCache) != 0 { + n := copy(b, c.peerCache) + c.peerCache = c.peerCache[n:] + return n, nil + } + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + peerLength, err := decodeHeader(peerHeader) // 17~17000 + if err != nil { + if c.instance != nil { + c.instance.Lock() + if bytes.Equal(c.reuse, c.instance.reuse) { + c.instance.expire = time.Now() // expired + } + c.instance.Unlock() + } + return 0, err + } + peerData := make([]byte, peerLength) + if _, err := io.ReadFull(c.Conn, peerData); err != nil { + return 0, err + } + dst := peerData[:peerLength-16] + if len(dst) <= len(b) { + dst = b[:len(dst)] // max=8192 is recommended for peer + } + _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) + increaseNonce(c.peerNonce) + 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 +} diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go new file mode 100644 index 000000000000..07edeae9f50e --- /dev/null +++ b/proxy/vless/encryption/common.go @@ -0,0 +1,55 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "strconv" + + "github.com/xtls/xray-core/common/errors" + "golang.org/x/crypto/chacha20poly1305" +) + +func encodeHeader(b []byte, l int) { + b[0] = 23 + b[1] = 3 + b[2] = 3 + b[3] = byte(l >> 8) + b[4] = byte(l) +} + +func decodeHeader(b []byte) (int, error) { + if b[0] == 23 && b[1] == 3 && b[2] == 3 { + l := int(b[3])<<8 | int(b[4]) + if l < 17 || l > 17000 { // TODO + return 0, errors.New("invalid length in record's header: " + strconv.Itoa(l)) + } + return l, nil + } + return 0, errors.New("invalid record's header") +} + +func newAead(c byte, k []byte) cipher.AEAD { + switch c { + case 0: + if block, err := aes.NewCipher(k); err == nil { + aead, _ := cipher.NewGCM(block) + return aead + } + case 1: + aead, _ := chacha20poly1305.New(k) + return aead + } + return nil +} + +func increaseNonce(nonce []byte) { + for i := range 12 { + nonce[11-i]++ + if nonce[11-i] != 0 { + break + } + if i == 11 { + // TODO + } + } +} diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go new file mode 100644 index 000000000000..7e7819f72c4c --- /dev/null +++ b/proxy/vless/encryption/server.go @@ -0,0 +1,251 @@ +package encryption + +import ( + "bytes" + "crypto/cipher" + "crypto/mlkem" + "crypto/rand" + "crypto/sha256" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "golang.org/x/crypto/hkdf" +) + +type ServerSession struct { + expire time.Time + cipher byte + baseKey []byte + randoms sync.Map +} + +type ServerInstance struct { + sync.RWMutex + dKeyNfs *mlkem.DecapsulationKey768 + minutes time.Duration + sessions map[[21]byte]*ServerSession +} + +type ServerConn struct { + net.Conn + cipher byte + baseKey []byte + reuse []byte + peerRandom []byte + peerAead cipher.AEAD + peerNonce []byte + peerCache []byte + aead cipher.AEAD + nonce []byte +} + +func (i *ServerInstance) Init(dKeyNfsData []byte, minutes time.Duration) (err error) { + i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData) + if minutes > 0 { + i.minutes = minutes + i.sessions = make(map[[21]byte]*ServerSession) + go func() { + for { + time.Sleep(time.Minute) + now := time.Now() + i.Lock() + for index, session := range i.sessions { + if now.After(session.expire) { + delete(i.sessions, index) + } + } + i.Unlock() + } + }() + } + return +} + +func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { + if i.dKeyNfs == nil { + return nil, errors.New("uninitialized") + } + c := &ServerConn{Conn: conn} + + peerReuseHello := make([]byte, 21+32) + if _, err := io.ReadFull(c.Conn, peerReuseHello); err != nil { + return nil, err + } + if i.minutes > 0 { + i.RLock() + s := i.sessions[[21]byte(peerReuseHello)] + i.RUnlock() + if s != nil { + if _, replay := s.randoms.LoadOrStore([32]byte(peerReuseHello[21:]), true); !replay { + c.cipher = s.cipher + c.baseKey = s.baseKey + c.reuse = peerReuseHello[:21] + c.peerRandom = peerReuseHello[21:] + return c, nil + } + } + } + + peerHeader := make([]byte, 5) + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return nil, err + } + if l, _ := decodeHeader(peerHeader); l != 0 { + c.Conn.Write(make([]byte, crypto.RandBetween(100, 1000))) // make client do new handshake + return nil, errors.New("invalid reuse") + } + + peerClientHello := make([]byte, 1088+1184+1) + copy(peerClientHello, peerReuseHello) + copy(peerClientHello[53:], peerHeader) + if _, err := io.ReadFull(c.Conn, peerClientHello[58:]); err != nil { + return nil, err + } + encapsulatedNfsKey := peerClientHello[:1088] + eKeyPfsData := peerClientHello[1088:2272] + c.cipher = peerClientHello[2272] + if c.cipher != 0 && c.cipher != 1 { + return nil, errors.New("invalid cipher") + } + + nfsKey, err := i.dKeyNfs.Decapsulate(encapsulatedNfsKey) + if err != nil { + return nil, err + } + eKeyPfs, err := mlkem.NewEncapsulationKey768(eKeyPfsData) + if err != nil { + return nil, err + } + pfsKey, encapsulatedPfsKey := eKeyPfs.Encapsulate() + c.baseKey = append(nfsKey, pfsKey...) + + authKey := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfsData).Read(authKey) + nonce := make([]byte, 12) + c.reuse = newAead(c.cipher, authKey).Seal(nil, nonce, []byte("VLESS"), encapsulatedPfsKey) + + padding := crypto.RandBetween(100, 1000) + + serverHello := make([]byte, 1088+21+5+padding) + copy(serverHello, encapsulatedPfsKey) + copy(serverHello[1088:], c.reuse) + encodeHeader(serverHello[1109:], int(padding)) + + if _, err := c.Conn.Write(serverHello); err != nil { + return nil, err + } + + if i.minutes > 0 { + i.Lock() + i.sessions[[21]byte(c.reuse)] = &ServerSession{ + expire: time.Now().Add(i.minutes), + cipher: c.cipher, + baseKey: c.baseKey, + } + i.Unlock() + } + + return c, nil +} + +func (c *ServerConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + peerHeader := make([]byte, 5) + if c.peerAead == nil { + if c.peerRandom == nil { + for { + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + peerPadding, _ := decodeHeader(peerHeader) + if peerPadding == 0 { + break + } + if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil { + return 0, err + } + } + peerIndex := make([]byte, 21) + copy(peerIndex, peerHeader) + if _, err := io.ReadFull(c.Conn, peerIndex[5:]); err != nil { + return 0, err + } + if !bytes.Equal(peerIndex, c.reuse) { + return 0, errors.New("naughty boy") + } + c.peerRandom = make([]byte, 32) + if _, err := io.ReadFull(c.Conn, c.peerRandom); err != nil { + return 0, err + } + } + peerKey := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, c.peerRandom, c.reuse).Read(peerKey) + c.peerAead = newAead(c.cipher, peerKey) + c.peerNonce = make([]byte, 12) + } + if len(c.peerCache) != 0 { + n := copy(b, c.peerCache) + c.peerCache = c.peerCache[n:] + return n, nil + } + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + peerLength, err := decodeHeader(peerHeader) // 17~17000 + if err != nil { + return 0, err + } + peerData := make([]byte, peerLength) + if _, err := io.ReadFull(c.Conn, peerData); err != nil { + return 0, err + } + dst := peerData[:peerLength-16] + if len(dst) <= len(b) { + dst = b[:len(dst)] // max=8192 is recommended for peer + } + _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) + increaseNonce(c.peerNonce) + if err != nil { + return 0, errors.New("error") + } + if len(dst) > len(b) { + c.peerCache = dst[copy(b, dst):] + dst = b // for len(dst) + } + return len(dst), nil +} + +func (c *ServerConn) Write(b []byte) (int, error) { // after first Read() + if len(b) == 0 { + return 0, nil + } + var data []byte + if c.aead == nil { + if c.peerRandom == nil { + return 0, errors.New("can not Write() first") + } + data = make([]byte, 32+5+len(b)+16) + rand.Read(data[:32]) + key := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, data[:32], c.peerRandom).Read(key) + c.aead = newAead(c.cipher, key) + c.nonce = make([]byte, 12) + encodeHeader(data[32:], len(b)+16) + c.aead.Seal(data[:37], c.nonce, b, data[32:37]) + } else { + data = make([]byte, 5+len(b)+16) + encodeHeader(data, len(b)+16) + c.aead.Seal(data[:5], c.nonce, b, data[:5]) + } + increaseNonce(c.nonce) + if _, err := c.Conn.Write(data); err != nil { + return 0, err + } + return len(b), nil +} diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index 907a3f7f3979..20837a4c4dee 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -111,11 +111,10 @@ 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"` + Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Config) Reset() { @@ -155,6 +154,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 +168,11 @@ func (x *Config) GetDecryption() string { return "" } -func (x *Config) GetFallbacks() []*Fallback { +func (x *Config) GetMinutes() uint32 { if x != nil { - return x.Fallbacks + return x.Minutes } - return nil + return 0 } var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor @@ -185,25 +191,26 @@ 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, 0xba, 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, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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..dc6bbac58f89 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -19,8 +19,8 @@ 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 minutes = 4; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index df1f9f3cae86..83af17e8966e 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,14 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } + d, _ := base64.RawURLEncoding.DecodeString(config.Decryption) + if len(d) == 64 { + handler.decryption = &encryption.ServerInstance{} + if err := handler.decryption.Init(d, time.Duration(config.Minutes)*time.Minute); err != nil { + return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() + } + } + if config.Fallbacks != nil { handler.fallbacks = make(map[string]map[string]map[string]*Fallback) // handler.regexps = make(map[string]*regexp.Regexp) @@ -204,6 +215,14 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s return errors.New("unable to set read deadline").Base(err).AtWarning() } + 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() + } + } + first := buf.FromBytes(make([]byte, buf.Size)) first.Clear() firstLen, errR := first.ReadFrom(connection) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index e1a727eb120a..5193e60a4a50 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -4,6 +4,7 @@ import ( "bytes" "context" gotls "crypto/tls" + "encoding/base64" "reflect" "time" "unsafe" @@ -24,6 +25,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 +45,7 @@ type Handler struct { serverPicker protocol.ServerPicker policyManager policy.Manager cone bool + encryption *encryption.ClientInstance } // New creates a new VLess outbound handler. @@ -64,6 +67,15 @@ func New(ctx context.Context, config *Config) (*Handler, error) { cone: ctx.Value("cone").(bool), } + a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) + e, _ := base64.RawURLEncoding.DecodeString(a.Encryption) + if len(e) == 1184 { + handler.encryption = &encryption.ClientInstance{} + if err := handler.encryption.Init(e, time.Duration(a.Minutes)*time.Minute); err != nil { + return nil, errors.New("failed to use mlkem768client").Base(err).AtError() + } + } + return handler, nil } @@ -98,6 +110,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 From fc137d26121b0b1fef6628569c79f1b9249587a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 10 Aug 2025 12:12:58 +0000 Subject: [PATCH 02/25] Fix test --- infra/conf/vless_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/conf/vless_test.go b/infra/conf/vless_test.go index 8f1baaa5c4e2..85f3acc4d758 100644 --- a/infra/conf/vless_test.go +++ b/infra/conf/vless_test.go @@ -104,7 +104,6 @@ func TestVLessInbound(t *testing.T) { Email: "love@example.com", }, }, - Decryption: "none", Fallbacks: []*inbound.Fallback{ { Alpn: "", From 3e19bf9233bdd9bafc073a71c65b737cc1ffba5e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 11 Aug 2025 00:24:08 +0000 Subject: [PATCH 03/25] Rename reuse/index -> ticket --- infra/conf/vless.go | 11 ++++++----- proxy/vless/encryption/client.go | 18 ++++++++--------- proxy/vless/encryption/server.go | 34 ++++++++++++++++---------------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index ed090facc927..d6f3727f1de9 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -65,7 +65,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } - if len(account.Encryption) > 0 { + if account.Encryption != "" { return nil, errors.New(`VLESS clients: "encryption" should not in inbound settings`) } @@ -73,8 +73,9 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Clients[idx] = user } + config.Decryption = c.Decryption if !func() bool { - s := strings.Split(c.Decryption, "-mlkem768seed-") + s := strings.Split(config.Decryption, "-mlkem768seed-") if len(s) != 2 { return false } @@ -95,11 +96,11 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { } config.Decryption = s[1] return true - }() && c.Decryption != "none" { - if c.Decryption == "" { + }() && 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": ` + c.Decryption) + return nil, errors.New(`VLESS settings: unsupported "decryption": ` + config.Decryption) } for _, fb := range c.Fallbacks { diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 425b1d007ad6..412403939566 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -31,14 +31,14 @@ type ClientInstance struct { minutes time.Duration expire time.Time baseKey []byte - reuse []byte + ticket []byte } type ClientConn struct { net.Conn instance *ClientInstance baseKey []byte - reuse []byte + ticket []byte random []byte aead cipher.AEAD nonce []byte @@ -64,7 +64,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if time.Now().Before(i.expire) { c.instance = i c.baseKey = i.baseKey - c.reuse = i.reuse + c.ticket = i.ticket i.RUnlock() return c, nil } @@ -94,7 +94,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { return nil, err } encapsulatedPfsKey := peerServerHello[:1088] - c.reuse = peerServerHello[1088:] + c.ticket = peerServerHello[1088:] pfsKey, err := dKeyPfs.Decapsulate(encapsulatedPfsKey) if err != nil { @@ -105,7 +105,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { authKey := make([]byte, 32) hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfs).Read(authKey) nonce := make([]byte, 12) - VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce, c.reuse, encapsulatedPfsKey) + VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce, c.ticket, encapsulatedPfsKey) if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more message return nil, errors.New("invalid server").AtError() } @@ -114,7 +114,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { i.Lock() i.expire = time.Now().Add(i.minutes) i.baseKey = c.baseKey - i.reuse = c.reuse + i.ticket = c.ticket i.Unlock() } @@ -130,12 +130,12 @@ func (c *ClientConn) Write(b []byte) (int, error) { c.random = make([]byte, 32) rand.Read(c.random) key := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, c.random, c.reuse).Read(key) + hkdf.New(sha256.New, c.baseKey, c.random, c.ticket).Read(key) c.aead = newAead(ClientCipher, key) c.nonce = make([]byte, 12) data = make([]byte, 21+32+5+len(b)+16) - copy(data, c.reuse) + copy(data, c.ticket) copy(data[21:], c.random) encodeHeader(data[53:], len(b)+16) c.aead.Seal(data[:58], c.nonce, b, data[53:58]) @@ -200,7 +200,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() if err != nil { if c.instance != nil { c.instance.Lock() - if bytes.Equal(c.reuse, c.instance.reuse) { + if bytes.Equal(c.ticket, c.instance.ticket) { c.instance.expire = time.Now() // expired } c.instance.Unlock() diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 7e7819f72c4c..4c36570671ae 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -34,7 +34,7 @@ type ServerConn struct { net.Conn cipher byte baseKey []byte - reuse []byte + ticket []byte peerRandom []byte peerAead cipher.AEAD peerNonce []byte @@ -71,20 +71,20 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { } c := &ServerConn{Conn: conn} - peerReuseHello := make([]byte, 21+32) - if _, err := io.ReadFull(c.Conn, peerReuseHello); err != nil { + peerTicketHello := make([]byte, 21+32) + if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { return nil, err } if i.minutes > 0 { i.RLock() - s := i.sessions[[21]byte(peerReuseHello)] + s := i.sessions[[21]byte(peerTicketHello)] i.RUnlock() if s != nil { - if _, replay := s.randoms.LoadOrStore([32]byte(peerReuseHello[21:]), true); !replay { + if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[21:]), true); !replay { c.cipher = s.cipher c.baseKey = s.baseKey - c.reuse = peerReuseHello[:21] - c.peerRandom = peerReuseHello[21:] + c.ticket = peerTicketHello[:21] + c.peerRandom = peerTicketHello[21:] return c, nil } } @@ -96,11 +96,11 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { } if l, _ := decodeHeader(peerHeader); l != 0 { c.Conn.Write(make([]byte, crypto.RandBetween(100, 1000))) // make client do new handshake - return nil, errors.New("invalid reuse") + return nil, errors.New("invalid ticket") } peerClientHello := make([]byte, 1088+1184+1) - copy(peerClientHello, peerReuseHello) + copy(peerClientHello, peerTicketHello) copy(peerClientHello[53:], peerHeader) if _, err := io.ReadFull(c.Conn, peerClientHello[58:]); err != nil { return nil, err @@ -126,13 +126,13 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { authKey := make([]byte, 32) hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfsData).Read(authKey) nonce := make([]byte, 12) - c.reuse = newAead(c.cipher, authKey).Seal(nil, nonce, []byte("VLESS"), encapsulatedPfsKey) + c.ticket = newAead(c.cipher, authKey).Seal(nil, nonce, []byte("VLESS"), encapsulatedPfsKey) padding := crypto.RandBetween(100, 1000) serverHello := make([]byte, 1088+21+5+padding) copy(serverHello, encapsulatedPfsKey) - copy(serverHello[1088:], c.reuse) + copy(serverHello[1088:], c.ticket) encodeHeader(serverHello[1109:], int(padding)) if _, err := c.Conn.Write(serverHello); err != nil { @@ -141,7 +141,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.minutes > 0 { i.Lock() - i.sessions[[21]byte(c.reuse)] = &ServerSession{ + i.sessions[[21]byte(c.ticket)] = &ServerSession{ expire: time.Now().Add(i.minutes), cipher: c.cipher, baseKey: c.baseKey, @@ -171,12 +171,12 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, err } } - peerIndex := make([]byte, 21) - copy(peerIndex, peerHeader) - if _, err := io.ReadFull(c.Conn, peerIndex[5:]); err != nil { + peerTicket := make([]byte, 21) + copy(peerTicket, peerHeader) + if _, err := io.ReadFull(c.Conn, peerTicket[5:]); err != nil { return 0, err } - if !bytes.Equal(peerIndex, c.reuse) { + if !bytes.Equal(peerTicket, c.ticket) { return 0, errors.New("naughty boy") } c.peerRandom = make([]byte, 32) @@ -185,7 +185,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } } peerKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, c.peerRandom, c.reuse).Read(peerKey) + hkdf.New(sha256.New, c.baseKey, c.peerRandom, c.ticket).Read(peerKey) c.peerAead = newAead(c.cipher, peerKey) c.peerNonce = make([]byte, 12) } From 2e6a88307cc48d5d47c284896912936cafc6dfdc Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 11 Aug 2025 00:28:09 +0000 Subject: [PATCH 04/25] Revert "Fix test" --- infra/conf/vless_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/conf/vless_test.go b/infra/conf/vless_test.go index 85f3acc4d758..8f1baaa5c4e2 100644 --- a/infra/conf/vless_test.go +++ b/infra/conf/vless_test.go @@ -104,6 +104,7 @@ func TestVLessInbound(t *testing.T) { Email: "love@example.com", }, }, + Decryption: "none", Fallbacks: []*inbound.Fallback{ { Alpn: "", From 7ffb555fc8ec51bd1e3e60f26f1d6957984dba80 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:07:00 +0000 Subject: [PATCH 05/25] Add optional aes128xor layer https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3172703168 --- infra/conf/vless.go | 30 +++++++++++---- proxy/vless/account.go | 3 ++ proxy/vless/account.pb.go | 31 ++++++++++------ proxy/vless/account.proto | 3 +- proxy/vless/encryption/client.go | 7 +++- proxy/vless/encryption/server.go | 7 +++- proxy/vless/encryption/xor.go | 63 ++++++++++++++++++++++++++++++++ proxy/vless/inbound/config.pb.go | 31 ++++++++++------ proxy/vless/inbound/config.proto | 3 +- proxy/vless/inbound/inbound.go | 2 +- proxy/vless/outbound/outbound.go | 2 +- 11 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 proxy/vless/encryption/xor.go diff --git a/infra/conf/vless.go b/infra/conf/vless.go index d6f3727f1de9..301e28bf54e9 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -75,8 +75,8 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Decryption = c.Decryption if !func() bool { - s := strings.Split(config.Decryption, "-mlkem768seed-") - if len(s) != 2 { + s := strings.SplitN(config.Decryption, "-", 4) + if len(s) != 4 || s[2] != "mlkem768seed" { return false } if s[0] != "1rtt" { @@ -90,11 +90,18 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { } config.Minutes = uint32(i) } - b, err := base64.RawURLEncoding.DecodeString(s[1]) + switch s[1] { + case "vless": + case "aes128xor": + config.Xor = 1 + default: + return false + } + b, err := base64.RawURLEncoding.DecodeString(s[3]) if len(b) != 64 || err != nil { return false } - config.Decryption = s[1] + config.Decryption = s[3] return true }() && config.Decryption != "none" { if config.Decryption == "" { @@ -216,8 +223,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } if !func() bool { - s := strings.Split(account.Encryption, "-mlkem768client-") - if len(s) != 2 { + s := strings.SplitN(account.Encryption, "-", 4) + if len(s) != 4 || s[2] != "mlkem768client" { return false } if s[0] != "1rtt" { @@ -231,11 +238,18 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } account.Minutes = uint32(i) } - b, err := base64.RawURLEncoding.DecodeString(s[1]) + switch s[1] { + case "vless": + case "aes128xor": + account.Xor = 1 + default: + return false + } + b, err := base64.RawURLEncoding.DecodeString(s[3]) if len(b) != 1184 || err != nil { return false } - account.Encryption = s[1] + account.Encryption = s[3] return true }() && account.Encryption != "none" { if account.Encryption == "" { diff --git a/proxy/vless/account.go b/proxy/vless/account.go index 71b2f27405c8..55c7b54c8db6 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,6 +18,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? + Xor: a.Xor, Minutes: a.Minutes, }, nil } @@ -30,6 +31,7 @@ type MemoryAccount struct { Flow string Encryption string + Xor uint32 Minutes uint32 } @@ -47,6 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, + Xor: a.Xor, Minutes: a.Minutes, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index be718d29fde9..6cc66f04cc9f 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -30,7 +30,8 @@ type Account struct { // Flow settings. May be "xtls-rprx-vision". Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` - Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` + Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Account) Reset() { @@ -84,6 +85,13 @@ func (x *Account) GetEncryption() string { return "" } +func (x *Account) GetXor() uint32 { + if x != nil { + return x.Xor + } + return 0 +} + func (x *Account) GetMinutes() uint32 { if x != nil { return x.Minutes @@ -96,20 +104,21 @@ 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, 0x67, 0x0a, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x79, 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, - 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, - 0x69, 0x6e, 0x75, 0x74, 0x65, 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, + 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, + 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, 0x6f, 0x72, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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 1b82a836a60d..199005adee6a 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -13,5 +13,6 @@ message Account { string flow = 2; string encryption = 3; - uint32 minutes = 4; + uint32 xor = 4; + uint32 minutes = 5; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 412403939566..d253eaaadbaa 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -28,6 +28,7 @@ func init() { type ClientInstance struct { sync.RWMutex eKeyNfs *mlkem.EncapsulationKey768 + xor uint32 minutes time.Duration expire time.Time baseKey []byte @@ -47,8 +48,9 @@ type ClientConn struct { peerCache []byte } -func (i *ClientInstance) Init(eKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ClientInstance) Init(eKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData) + i.xor = xor i.minutes = minutes return } @@ -57,6 +59,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.eKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.eKeyNfs.Bytes()) + } c := &ClientConn{Conn: conn} if i.minutes > 0 { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 4c36570671ae..6507c8c03864 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -26,6 +26,7 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex dKeyNfs *mlkem.DecapsulationKey768 + xor uint32 minutes time.Duration sessions map[[21]byte]*ServerSession } @@ -43,8 +44,9 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(dKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData) + i.xor = xor if minutes > 0 { i.minutes = minutes i.sessions = make(map[[21]byte]*ServerSession) @@ -69,6 +71,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.dKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.dKeyNfs.EncapsulationKey().Bytes()) + } c := &ServerConn{Conn: conn} peerTicketHello := make([]byte, 21+32) diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go new file mode 100644 index 000000000000..c8af2112d16e --- /dev/null +++ b/proxy/vless/encryption/xor.go @@ -0,0 +1,63 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +type XorConn struct { + net.Conn + key []byte + ctr cipher.Stream + peerCtr cipher.Stream +} + +func NewXorConn(conn net.Conn, key []byte) *XorConn { + return &XorConn{Conn: conn, key: key[:16]} +} + +func (c *XorConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + var iv []byte + if c.ctr == nil { + block, _ := aes.NewCipher(c.key) + iv = make([]byte, 16) + rand.Read(iv) + c.ctr = cipher.NewCTR(block, iv) + } + c.ctr.XORKeyStream(b, b) // caller MUST discard b + if iv != nil { + b = append(iv, b...) + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + if iv != nil { + b = b[16:] + } + return len(b), nil +} + +func (c *XorConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + if c.peerCtr == nil { + peerIv := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, peerIv); err != nil { + return 0, err + } + block, _ := aes.NewCipher(c.key) + c.peerCtr = cipher.NewCTR(block, peerIv) + } + n, err := c.Conn.Read(b) + if n > 0 { + c.peerCtr.XORKeyStream(b[:n], b[:n]) + } + return n, err +} diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index 20837a4c4dee..a125a7e42646 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -114,7 +114,8 @@ type Config struct { 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"` - Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` + Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Config) Reset() { @@ -168,6 +169,13 @@ func (x *Config) GetDecryption() string { return "" } +func (x *Config) GetXor() uint32 { + if x != nil { + return x.Xor + } + return 0 +} + func (x *Config) GetMinutes() uint32 { if x != nil { return x.Minutes @@ -191,7 +199,7 @@ 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, 0xba, 0x01, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xcc, 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, @@ -202,15 +210,16 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 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, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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, 0x10, 0x0a, 0x03, 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, + 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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 dc6bbac58f89..c96855a0619c 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -22,5 +22,6 @@ message Config { repeated Fallback fallbacks = 2; string decryption = 3; - uint32 minutes = 4; + uint32 xor = 4; + uint32 minutes = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 83af17e8966e..7c5e9005512d 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -87,7 +87,7 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val d, _ := base64.RawURLEncoding.DecodeString(config.Decryption) if len(d) == 64 { handler.decryption = &encryption.ServerInstance{} - if err := handler.decryption.Init(d, time.Duration(config.Minutes)*time.Minute); err != nil { + if err := handler.decryption.Init(d, config.Xor, time.Duration(config.Minutes)*time.Minute); err != nil { return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() } } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 5193e60a4a50..1bbd879b89d6 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -71,7 +71,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) { e, _ := base64.RawURLEncoding.DecodeString(a.Encryption) if len(e) == 1184 { handler.encryption = &encryption.ClientInstance{} - if err := handler.encryption.Init(e, time.Duration(a.Minutes)*time.Minute); err != nil { + if err := handler.encryption.Init(e, a.Xor, time.Duration(a.Minutes)*time.Minute); err != nil { return nil, errors.New("failed to use mlkem768client").Base(err).AtError() } } From ec1cc35188c1a5f38a2ff75e88b5d043ffdc59da Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:51:28 +0000 Subject: [PATCH 06/25] Mainly reverse VLESS Client Hello https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3176751311 --- proxy/vless/encryption/client.go | 126 +++++++++++++++++-------------- proxy/vless/encryption/common.go | 18 ++--- proxy/vless/encryption/server.go | 122 ++++++++++++++++-------------- proxy/vless/encryption/xor.go | 2 +- 4 files changed, 144 insertions(+), 124 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index d253eaaadbaa..fb21e47d5c14 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -20,19 +20,20 @@ import ( var ClientCipher byte func init() { - if !protocol.HasAESGCMHardwareSupport { + if protocol.HasAESGCMHardwareSupport { ClientCipher = 1 } } type ClientInstance struct { sync.RWMutex - eKeyNfs *mlkem.EncapsulationKey768 - xor uint32 - minutes time.Duration - expire time.Time - baseKey []byte - ticket []byte + nfsEKey *mlkem.EncapsulationKey768 + nfsEKeyBytes []byte + xor uint32 + minutes time.Duration + expire time.Time + baseKey []byte + ticket []byte } type ClientConn struct { @@ -48,19 +49,22 @@ type ClientConn struct { peerCache []byte } -func (i *ClientInstance) Init(eKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { - i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData) - i.xor = xor +func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { + i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes) + if xor > 0 { + i.nfsEKeyBytes = nfsEKeyBytes + i.xor = xor + } i.minutes = minutes return } func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { - if i.eKeyNfs == nil { + if i.nfsEKey == nil { return nil, errors.New("uninitialized") } - if i.xor == 1 { - conn = NewXorConn(conn, i.eKeyNfs.Bytes()) + if i.xor > 0 { + conn = NewXorConn(conn, i.nfsEKeyBytes) } c := &ClientConn{Conn: conn} @@ -76,18 +80,19 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { i.RUnlock() } - nfsKey, encapsulatedNfsKey := i.eKeyNfs.Encapsulate() - seed := make([]byte, 64) - rand.Read(seed) - dKeyPfs, _ := mlkem.NewDecapsulationKey768(seed) - eKeyPfs := dKeyPfs.EncapsulationKey().Bytes() - padding := crypto.RandBetween(100, 1000) + pfsDKeySeed := make([]byte, 64) + rand.Read(pfsDKeySeed) + pfsDKey, _ := mlkem.NewDecapsulationKey768(pfsDKeySeed) + pfsEKeyBytes := pfsDKey.EncapsulationKey().Bytes() + nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate() + paddingLen := crypto.RandBetween(100, 1000) - clientHello := make([]byte, 1088+1184+1+5+padding) - copy(clientHello, encapsulatedNfsKey) - copy(clientHello[1088:], eKeyPfs) - clientHello[2272] = ClientCipher - encodeHeader(clientHello[2273:], int(padding)) + clientHello := make([]byte, 1+1184+1088+5+paddingLen) + clientHello[0] = ClientCipher + copy(clientHello[1:], pfsEKeyBytes) + copy(clientHello[1185:], encapsulatedNfsKey) + encodeHeader(clientHello[2273:], int(paddingLen)) + rand.Read(clientHello[2278:]) if _, err := c.Conn.Write(clientHello); err != nil { return nil, err @@ -101,16 +106,16 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { encapsulatedPfsKey := peerServerHello[:1088] c.ticket = peerServerHello[1088:] - pfsKey, err := dKeyPfs.Decapsulate(encapsulatedPfsKey) + pfsKey, err := pfsDKey.Decapsulate(encapsulatedPfsKey) if err != nil { return nil, err } - c.baseKey = append(nfsKey, pfsKey...) + c.baseKey = append(pfsKey, nfsKey...) authKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfs).Read(authKey) - nonce := make([]byte, 12) - VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce, c.ticket, encapsulatedPfsKey) + hkdf.New(sha256.New, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Read(authKey) + nonce := [12]byte{ClientCipher} + VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce[:], c.ticket, pfsEKeyBytes) if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more message return nil, errors.New("invalid server").AtError() } @@ -130,33 +135,40 @@ func (c *ClientConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - var data []byte - if c.aead == nil { - c.random = make([]byte, 32) - rand.Read(c.random) - key := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, c.random, c.ticket).Read(key) - c.aead = newAead(ClientCipher, key) - c.nonce = make([]byte, 12) - - data = make([]byte, 21+32+5+len(b)+16) - copy(data, c.ticket) - copy(data[21:], c.random) - encodeHeader(data[53:], len(b)+16) - c.aead.Seal(data[:58], c.nonce, b, data[53:58]) - } else { - data = make([]byte, 5+len(b)+16) - encodeHeader(data, len(b)+16) - c.aead.Seal(data[:5], c.nonce, b, data[:5]) - } - increaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { - return 0, err + for n := 0; n < len(b); { + b := b[n:] + if len(b) > 8192 { + b = b[:8192] // for avoiding another copy() in server's Read() + } + n += len(b) + var data []byte + if c.aead == nil { + c.random = make([]byte, 32) + rand.Read(c.random) + key := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, c.random, c.ticket).Read(key) + c.aead = newAead(ClientCipher, key) + c.nonce = make([]byte, 12) + + data = make([]byte, 21+32+5+len(b)+16) + copy(data, c.ticket) + copy(data[21:], c.random) + encodeHeader(data[53:], len(b)+16) + c.aead.Seal(data[:58], c.nonce, b, data[53:58]) + } else { + data = make([]byte, 5+len(b)+16) + encodeHeader(data, len(b)+16) + c.aead.Seal(data[:5], c.nonce, b, data[:5]) + } + increaseNonce(c.nonce) + if _, err := c.Conn.Write(data); err != nil { + return 0, err + } } return len(b), nil } -func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() +func (c *ClientConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } @@ -167,11 +179,11 @@ func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerPadding, _ := decodeHeader(peerHeader) - if peerPadding == 0 { + peerPaddingLen, _ := decodeHeader(peerHeader) + if peerPaddingLen == 0 { break } - if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil { + if _, err := io.ReadFull(c.Conn, make([]byte, peerPaddingLen)); err != nil { return 0, err } } @@ -186,7 +198,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() return 0, err } if c.random == nil { - return 0, errors.New("can not Read() first") + return 0, errors.New("empty c.random") } peerKey := make([]byte, 32) hkdf.New(sha256.New, c.baseKey, peerRandom, c.random).Read(peerKey) @@ -218,7 +230,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { // after first Write() } dst := peerData[:peerLength-16] if len(dst) <= len(b) { - dst = b[:len(dst)] // max=8192 is recommended for peer + dst = b[:len(dst)] // avoids another copy() } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) increaseNonce(c.peerNonce) diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 07edeae9f50e..f3a5425e7ace 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -28,18 +28,14 @@ func decodeHeader(b []byte) (int, error) { return 0, errors.New("invalid record's header") } -func newAead(c byte, k []byte) cipher.AEAD { - switch c { - case 0: - if block, err := aes.NewCipher(k); err == nil { - aead, _ := cipher.NewGCM(block) - return aead - } - case 1: - aead, _ := chacha20poly1305.New(k) - return aead +func newAead(c byte, k []byte) (aead cipher.AEAD) { + if c&1 == 1 { + block, _ := aes.NewCipher(k) + aead, _ = cipher.NewGCM(block) + } else { + aead, _ = chacha20poly1305.New(k) } - return nil + return } func increaseNonce(nonce []byte) { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 6507c8c03864..88c3c354f7fa 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -25,10 +25,11 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex - dKeyNfs *mlkem.DecapsulationKey768 - xor uint32 - minutes time.Duration - sessions map[[21]byte]*ServerSession + nfsDKey *mlkem.DecapsulationKey768 + nfsEKeyBytes []byte + xor uint32 + minutes time.Duration + sessions map[[21]byte]*ServerSession } type ServerConn struct { @@ -44,9 +45,12 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { - i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData) - i.xor = xor +func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Duration) (err error) { + i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed) + if xor > 0 { + i.nfsEKeyBytes = i.nfsDKey.EncapsulationKey().Bytes() + i.xor = xor + } if minutes > 0 { i.minutes = minutes i.sessions = make(map[[21]byte]*ServerSession) @@ -55,9 +59,9 @@ func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Durat time.Sleep(time.Minute) now := time.Now() i.Lock() - for index, session := range i.sessions { + for ticket, session := range i.sessions { if now.After(session.expire) { - delete(i.sessions, index) + delete(i.sessions, ticket) } } i.Unlock() @@ -68,11 +72,11 @@ func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Durat } func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { - if i.dKeyNfs == nil { + if i.nfsDKey == nil { return nil, errors.New("uninitialized") } - if i.xor == 1 { - conn = NewXorConn(conn, i.dKeyNfs.EncapsulationKey().Bytes()) + if i.xor > 0 { + conn = NewXorConn(conn, i.nfsEKeyBytes) } c := &ServerConn{Conn: conn} @@ -100,49 +104,50 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { return nil, err } if l, _ := decodeHeader(peerHeader); l != 0 { - c.Conn.Write(make([]byte, crypto.RandBetween(100, 1000))) // make client do new handshake + noise := make([]byte, crypto.RandBetween(100, 1000)) + rand.Read(noise) + c.Conn.Write(noise) // make client do new handshake return nil, errors.New("invalid ticket") } - peerClientHello := make([]byte, 1088+1184+1) + peerClientHello := make([]byte, 1+1184+1088) copy(peerClientHello, peerTicketHello) copy(peerClientHello[53:], peerHeader) if _, err := io.ReadFull(c.Conn, peerClientHello[58:]); err != nil { return nil, err } - encapsulatedNfsKey := peerClientHello[:1088] - eKeyPfsData := peerClientHello[1088:2272] - c.cipher = peerClientHello[2272] - if c.cipher != 0 && c.cipher != 1 { - return nil, errors.New("invalid cipher") - } + c.cipher = peerClientHello[0] + pfsEKeyBytes := peerClientHello[1:1185] + encapsulatedNfsKey := peerClientHello[1185:2273] - nfsKey, err := i.dKeyNfs.Decapsulate(encapsulatedNfsKey) + pfsEKey, err := mlkem.NewEncapsulationKey768(pfsEKeyBytes) if err != nil { return nil, err } - eKeyPfs, err := mlkem.NewEncapsulationKey768(eKeyPfsData) + nfsKey, err := i.nfsDKey.Decapsulate(encapsulatedNfsKey) if err != nil { return nil, err } - pfsKey, encapsulatedPfsKey := eKeyPfs.Encapsulate() - c.baseKey = append(nfsKey, pfsKey...) + pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() + c.baseKey = append(pfsKey, nfsKey...) authKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, encapsulatedNfsKey, eKeyPfsData).Read(authKey) - nonce := make([]byte, 12) - c.ticket = newAead(c.cipher, authKey).Seal(nil, nonce, []byte("VLESS"), encapsulatedPfsKey) + hkdf.New(sha256.New, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Read(authKey) + nonce := [12]byte{c.cipher} + c.ticket = newAead(c.cipher, authKey).Seal(nil, nonce[:], []byte("VLESS"), pfsEKeyBytes) - padding := crypto.RandBetween(100, 1000) + paddingLen := crypto.RandBetween(100, 1000) - serverHello := make([]byte, 1088+21+5+padding) + serverHello := make([]byte, 1088+21+5+paddingLen) copy(serverHello, encapsulatedPfsKey) copy(serverHello[1088:], c.ticket) - encodeHeader(serverHello[1109:], int(padding)) + encodeHeader(serverHello[1109:], int(paddingLen)) + rand.Read(serverHello[1114:]) if _, err := c.Conn.Write(serverHello); err != nil { return nil, err } + // we can send more padding if needed if i.minutes > 0 { i.Lock() @@ -168,11 +173,11 @@ func (c *ServerConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerPadding, _ := decodeHeader(peerHeader) - if peerPadding == 0 { + peerPaddingLen, _ := decodeHeader(peerHeader) + if peerPaddingLen == 0 { break } - if _, err := io.ReadFull(c.Conn, make([]byte, peerPadding)); err != nil { + if _, err := io.ReadFull(c.Conn, make([]byte, peerPaddingLen)); err != nil { return 0, err } } @@ -212,7 +217,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } dst := peerData[:peerLength-16] if len(dst) <= len(b) { - dst = b[:len(dst)] // max=8192 is recommended for peer + dst = b[:len(dst)] // avoids another copy() } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) increaseNonce(c.peerNonce) @@ -226,31 +231,38 @@ func (c *ServerConn) Read(b []byte) (int, error) { return len(dst), nil } -func (c *ServerConn) Write(b []byte) (int, error) { // after first Read() +func (c *ServerConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - var data []byte - if c.aead == nil { - if c.peerRandom == nil { - return 0, errors.New("can not Write() first") + for n := 0; n < len(b); { + b := b[n:] + if len(b) > 8192 { + b = b[:8192] // for avoiding another copy() in client's Read() + } + n += len(b) + var data []byte + if c.aead == nil { + if c.peerRandom == nil { + return 0, errors.New("empty c.peerRandom") + } + data = make([]byte, 32+5+len(b)+16) + rand.Read(data[:32]) + key := make([]byte, 32) + hkdf.New(sha256.New, c.baseKey, data[:32], c.peerRandom).Read(key) + c.aead = newAead(c.cipher, key) + c.nonce = make([]byte, 12) + encodeHeader(data[32:], len(b)+16) + c.aead.Seal(data[:37], c.nonce, b, data[32:37]) + } else { + data = make([]byte, 5+len(b)+16) + encodeHeader(data, len(b)+16) + c.aead.Seal(data[:5], c.nonce, b, data[:5]) + } + increaseNonce(c.nonce) + if _, err := c.Conn.Write(data); err != nil { + return 0, err } - data = make([]byte, 32+5+len(b)+16) - rand.Read(data[:32]) - key := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, data[:32], c.peerRandom).Read(key) - c.aead = newAead(c.cipher, key) - c.nonce = make([]byte, 12) - encodeHeader(data[32:], len(b)+16) - c.aead.Seal(data[:37], c.nonce, b, data[32:37]) - } else { - data = make([]byte, 5+len(b)+16) - encodeHeader(data, len(b)+16) - c.aead.Seal(data[:5], c.nonce, b, data[:5]) - } - increaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { - return 0, err } return len(b), nil } diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index c8af2112d16e..296fdd637af2 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -38,7 +38,7 @@ func (c *XorConn) Write(b []byte) (int, error) { return 0, err } if iv != nil { - b = b[16:] + b = b[16:] // for len(b) } return len(b), nil } From 5c611420487a92f931faefc01d4bf03869f477f6 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:50:44 +0000 Subject: [PATCH 07/25] Generate new key when nonce reaches max value https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3179685937 --- proxy/vless/encryption/client.go | 43 ++++++++++++++++---------------- proxy/vless/encryption/common.go | 24 ++++++++++-------- proxy/vless/encryption/server.go | 42 ++++++++++++++++--------------- 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index fb21e47d5c14..04360e2b0b1d 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -5,7 +5,6 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" - "crypto/sha256" "io" "net" "sync" @@ -14,7 +13,6 @@ import ( "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/protocol" - "golang.org/x/crypto/hkdf" ) var ClientCipher byte @@ -91,7 +89,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { clientHello[0] = ClientCipher copy(clientHello[1:], pfsEKeyBytes) copy(clientHello[1185:], encapsulatedNfsKey) - encodeHeader(clientHello[2273:], int(paddingLen)) + EncodeHeader(clientHello[2273:], int(paddingLen)) rand.Read(clientHello[2278:]) if _, err := c.Conn.Write(clientHello); err != nil { @@ -112,11 +110,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { } c.baseKey = append(pfsKey, nfsKey...) - authKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Read(authKey) nonce := [12]byte{ClientCipher} - VLESS, _ := newAead(ClientCipher, authKey).Open(nil, nonce[:], c.ticket, pfsEKeyBytes) - if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more message + VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, nonce[:], c.ticket, pfsEKeyBytes) + if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more messages return nil, errors.New("invalid server").AtError() } @@ -135,32 +131,32 @@ func (c *ClientConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } + var data []byte for n := 0; n < len(b); { b := b[n:] if len(b) > 8192 { b = b[:8192] // for avoiding another copy() in server's Read() } n += len(b) - var data []byte if c.aead == nil { c.random = make([]byte, 32) rand.Read(c.random) - key := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, c.random, c.ticket).Read(key) - c.aead = newAead(ClientCipher, key) + c.aead = NewAead(ClientCipher, c.baseKey, c.random, c.ticket) c.nonce = make([]byte, 12) - data = make([]byte, 21+32+5+len(b)+16) copy(data, c.ticket) copy(data[21:], c.random) - encodeHeader(data[53:], len(b)+16) + EncodeHeader(data[53:], len(b)+16) c.aead.Seal(data[:58], c.nonce, b, data[53:58]) } else { data = make([]byte, 5+len(b)+16) - encodeHeader(data, len(b)+16) + EncodeHeader(data, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) + if bytes.Equal(c.nonce, MaxNonce) { + c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) + } } - increaseNonce(c.nonce) + IncreaseNonce(c.nonce) if _, err := c.Conn.Write(data); err != nil { return 0, err } @@ -179,7 +175,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerPaddingLen, _ := decodeHeader(peerHeader) + peerPaddingLen, _ := DecodeHeader(peerHeader) if peerPaddingLen == 0 { break } @@ -200,9 +196,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if c.random == nil { return 0, errors.New("empty c.random") } - peerKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, peerRandom, c.random).Read(peerKey) - c.peerAead = newAead(ClientCipher, peerKey) + c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandom, c.random) c.peerNonce = make([]byte, 12) } if len(c.peerCache) != 0 { @@ -213,7 +207,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerLength, err := decodeHeader(peerHeader) // 17~17000 + peerLength, err := DecodeHeader(peerHeader) // 17~17000 if err != nil { if c.instance != nil { c.instance.Lock() @@ -232,8 +226,15 @@ func (c *ClientConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } + var peerAead cipher.AEAD + if bytes.Equal(c.peerNonce, MaxNonce) { + peerAead = NewAead(ClientCipher, c.baseKey, peerData, peerHeader) + } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) - increaseNonce(c.peerNonce) + if peerAead != nil { + c.peerAead = peerAead + } + IncreaseNonce(c.peerNonce) if err != nil { return 0, err } diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index f3a5425e7ace..0cd23e16174a 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -1,15 +1,20 @@ package encryption import ( + "bytes" "crypto/aes" "crypto/cipher" + "crypto/sha256" "strconv" "github.com/xtls/xray-core/common/errors" "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" ) -func encodeHeader(b []byte, l int) { +var MaxNonce = bytes.Repeat([]byte{255}, 12) + +func EncodeHeader(b []byte, l int) { b[0] = 23 b[1] = 3 b[2] = 3 @@ -17,10 +22,10 @@ func encodeHeader(b []byte, l int) { b[4] = byte(l) } -func decodeHeader(b []byte) (int, error) { +func DecodeHeader(b []byte) (int, error) { if b[0] == 23 && b[1] == 3 && b[2] == 3 { l := int(b[3])<<8 | int(b[4]) - if l < 17 || l > 17000 { // TODO + if l < 17 || l > 17000 { // TODO: TLSv1.3 max length return 0, errors.New("invalid length in record's header: " + strconv.Itoa(l)) } return l, nil @@ -28,24 +33,23 @@ func decodeHeader(b []byte) (int, error) { return 0, errors.New("invalid record's header") } -func newAead(c byte, k []byte) (aead cipher.AEAD) { +func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { + key := make([]byte, 32) + hkdf.New(sha256.New, secret, salt, info).Read(key) if c&1 == 1 { - block, _ := aes.NewCipher(k) + block, _ := aes.NewCipher(key) aead, _ = cipher.NewGCM(block) } else { - aead, _ = chacha20poly1305.New(k) + aead, _ = chacha20poly1305.New(key) } return } -func increaseNonce(nonce []byte) { +func IncreaseNonce(nonce []byte) { for i := range 12 { nonce[11-i]++ if nonce[11-i] != 0 { break } - if i == 11 { - // TODO - } } } diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 88c3c354f7fa..63c0e8a73a4e 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -5,7 +5,6 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" - "crypto/sha256" "io" "net" "sync" @@ -13,7 +12,6 @@ import ( "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" - "golang.org/x/crypto/hkdf" ) type ServerSession struct { @@ -103,7 +101,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return nil, err } - if l, _ := decodeHeader(peerHeader); l != 0 { + if l, _ := DecodeHeader(peerHeader); l != 0 { noise := make([]byte, crypto.RandBetween(100, 1000)) rand.Read(noise) c.Conn.Write(noise) // make client do new handshake @@ -131,17 +129,15 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) - authKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Read(authKey) nonce := [12]byte{c.cipher} - c.ticket = newAead(c.cipher, authKey).Seal(nil, nonce[:], []byte("VLESS"), pfsEKeyBytes) + c.ticket = NewAead(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, nonce[:], []byte("VLESS"), pfsEKeyBytes) paddingLen := crypto.RandBetween(100, 1000) serverHello := make([]byte, 1088+21+5+paddingLen) copy(serverHello, encapsulatedPfsKey) copy(serverHello[1088:], c.ticket) - encodeHeader(serverHello[1109:], int(paddingLen)) + EncodeHeader(serverHello[1109:], int(paddingLen)) rand.Read(serverHello[1114:]) if _, err := c.Conn.Write(serverHello); err != nil { @@ -173,7 +169,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerPaddingLen, _ := decodeHeader(peerHeader) + peerPaddingLen, _ := DecodeHeader(peerHeader) if peerPaddingLen == 0 { break } @@ -194,9 +190,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, err } } - peerKey := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, c.peerRandom, c.ticket).Read(peerKey) - c.peerAead = newAead(c.cipher, peerKey) + c.peerAead = NewAead(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } if len(c.peerCache) != 0 { @@ -207,7 +201,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { return 0, err } - peerLength, err := decodeHeader(peerHeader) // 17~17000 + peerLength, err := DecodeHeader(peerHeader) // 17~17000 if err != nil { return 0, err } @@ -219,8 +213,15 @@ func (c *ServerConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } + var peerAead cipher.AEAD + if bytes.Equal(c.peerNonce, MaxNonce) { + peerAead = NewAead(ClientCipher, c.baseKey, peerData, peerHeader) + } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) - increaseNonce(c.peerNonce) + if peerAead != nil { + c.peerAead = peerAead + } + IncreaseNonce(c.peerNonce) if err != nil { return 0, errors.New("error") } @@ -235,31 +236,32 @@ func (c *ServerConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } + var data []byte for n := 0; n < len(b); { b := b[n:] if len(b) > 8192 { b = b[:8192] // for avoiding another copy() in client's Read() } n += len(b) - var data []byte if c.aead == nil { if c.peerRandom == nil { return 0, errors.New("empty c.peerRandom") } data = make([]byte, 32+5+len(b)+16) rand.Read(data[:32]) - key := make([]byte, 32) - hkdf.New(sha256.New, c.baseKey, data[:32], c.peerRandom).Read(key) - c.aead = newAead(c.cipher, key) + c.aead = NewAead(c.cipher, c.baseKey, data[:32], c.peerRandom) c.nonce = make([]byte, 12) - encodeHeader(data[32:], len(b)+16) + EncodeHeader(data[32:], len(b)+16) c.aead.Seal(data[:37], c.nonce, b, data[32:37]) } else { data = make([]byte, 5+len(b)+16) - encodeHeader(data, len(b)+16) + EncodeHeader(data, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) + if bytes.Equal(c.nonce, MaxNonce) { + c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) + } } - increaseNonce(c.nonce) + IncreaseNonce(c.nonce) if _, err := c.Conn.Write(data); err != nil { return 0, err } From 23d7aad461d232bc5bed52dd6aaa731ecd88ad35 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:07:57 +0000 Subject: [PATCH 08/25] Add Close() for ServerInstance; Fix server's nonce overflow https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3180075690 https://github.com/XTLS/Xray-core/commit/5c611420487a92f931faefc01d4bf03869f477f6#r163855798 --- proxy/vless/encryption/server.go | 16 ++++++++++++++-- proxy/vless/inbound/inbound.go | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 63c0e8a73a4e..1cf5b2f929e0 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -28,6 +28,7 @@ type ServerInstance struct { xor uint32 minutes time.Duration sessions map[[21]byte]*ServerSession + closed bool } type ServerConn struct { @@ -57,6 +58,10 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat time.Sleep(time.Minute) now := time.Now() i.Lock() + if i.closed { + i.Unlock() + return + } for ticket, session := range i.sessions { if now.After(session.expire) { delete(i.sessions, ticket) @@ -69,6 +74,13 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat return } +func (i *ServerInstance) Close() (err error) { + i.Lock() + i.closed = true + i.Unlock() + return +} + func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.nfsDKey == nil { return nil, errors.New("uninitialized") @@ -215,7 +227,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(ClientCipher, c.baseKey, peerData, peerHeader) + peerAead = NewAead(c.cipher, c.baseKey, peerData, peerHeader) } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) if peerAead != nil { @@ -258,7 +270,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { EncodeHeader(data, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) + c.aead = NewAead(c.cipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 7c5e9005512d..06c9e518f24c 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -170,6 +170,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)) } From 3c20bddfcfd8999be5f9a2ac180dc959950e4c61 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 13 Aug 2025 09:17:37 +0000 Subject: [PATCH 09/25] Add 5-bytes header for client/server/ticket hello and server random https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3182301703 --- proxy/vless/encryption/client.go | 103 ++++++++++++++---------- proxy/vless/encryption/common.go | 58 ++++++++++---- proxy/vless/encryption/server.go | 131 +++++++++++++++++-------------- 3 files changed, 179 insertions(+), 113 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 04360e2b0b1d..52b4828b0267 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -85,19 +85,31 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate() paddingLen := crypto.RandBetween(100, 1000) - clientHello := make([]byte, 1+1184+1088+5+paddingLen) - clientHello[0] = ClientCipher - copy(clientHello[1:], pfsEKeyBytes) - copy(clientHello[1185:], encapsulatedNfsKey) - EncodeHeader(clientHello[2273:], int(paddingLen)) - rand.Read(clientHello[2278:]) - - if _, err := c.Conn.Write(clientHello); err != nil { + clientHello := make([]byte, 5+1+1184+1088+5+paddingLen) + EncodeHeader(clientHello, 1, 1+1184+1088) + clientHello[5] = ClientCipher + copy(clientHello[5+1:], pfsEKeyBytes) + copy(clientHello[5+1+1184:], encapsulatedNfsKey) + EncodeHeader(clientHello[5+1+1184+1088:], 23, int(paddingLen)) + rand.Read(clientHello[5+1+1184+1088+5:]) + + if n, err := c.Conn.Write(clientHello); n != len(clientHello) || err != nil { + return nil, err + } + // client can send more padding / NFS AEAD messages if needed + + _, t, l, err := ReadAndDecodeHeader(c.Conn) + if err != nil { return nil, err } - // we can send more padding if needed + if t != 1 { + return nil, errors.New("unexpected type ", t, ", expect server hello") + } peerServerHello := make([]byte, 1088+21) + if l != len(peerServerHello) { + return nil, errors.New("unexpected length ", l, " for server hello") + } if _, err := io.ReadFull(c.Conn, peerServerHello); err != nil { return nil, err } @@ -112,7 +124,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { nonce := [12]byte{ClientCipher} VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, nonce[:], c.ticket, pfsEKeyBytes) - if !bytes.Equal(VLESS, []byte("VLESS")) { // TODO: more messages + if !bytes.Equal(VLESS, []byte("VLESS")) { return nil, errors.New("invalid server").AtError() } @@ -143,21 +155,22 @@ func (c *ClientConn) Write(b []byte) (int, error) { rand.Read(c.random) c.aead = NewAead(ClientCipher, c.baseKey, c.random, c.ticket) c.nonce = make([]byte, 12) - data = make([]byte, 21+32+5+len(b)+16) - copy(data, c.ticket) - copy(data[21:], c.random) - EncodeHeader(data[53:], len(b)+16) - c.aead.Seal(data[:58], c.nonce, b, data[53:58]) + data = make([]byte, 5+21+32+5+len(b)+16) + EncodeHeader(data, 0, 21+32) + copy(data[5:], c.ticket) + copy(data[5+21:], c.random) + EncodeHeader(data[5+21+32:], 23, len(b)+16) + c.aead.Seal(data[:5+21+32+5], c.nonce, b, data[5+21+32:5+21+32+5]) } else { data = make([]byte, 5+len(b)+16) - EncodeHeader(data, len(b)+16) + EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { + if n, err := c.Conn.Write(data); n != len(data) || err != nil { return 0, err } } @@ -168,29 +181,44 @@ func (c *ClientConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - peerHeader := make([]byte, 5) if c.peerAead == nil { - if c.instance == nil { + var t byte + var l int + var err error + if c.instance == nil { // 1-RTT for { - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + if _, t, l, err = ReadAndDecodeHeader(c.Conn); err != nil { return 0, err } - peerPaddingLen, _ := DecodeHeader(peerHeader) - if peerPaddingLen == 0 { + if t != 23 { break } - if _, err := io.ReadFull(c.Conn, make([]byte, peerPaddingLen)); err != nil { + if _, err := io.ReadFull(c.Conn, make([]byte, l)); err != nil { return 0, err } } } else { - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + h := make([]byte, 5) + if _, err := io.ReadFull(c.Conn, h); err != nil { return 0, err } + if t, l, err = DecodeHeader(h); err != nil { + c.instance.Lock() + if bytes.Equal(c.ticket, c.instance.ticket) { + c.instance.expire = time.Now() // expired + } + c.instance.Unlock() + return 0, errors.New("new handshake needed") + } + } + if t != 0 { + return 0, errors.New("unexpected type ", t, ", expect server random") } peerRandom := make([]byte, 32) - copy(peerRandom, peerHeader) - if _, err := io.ReadFull(c.Conn, peerRandom[5:]); err != nil { + if l != len(peerRandom) { + return 0, errors.New("unexpected length ", l, " for server random") + } + if _, err := io.ReadFull(c.Conn, peerRandom); err != nil { return 0, err } if c.random == nil { @@ -204,33 +232,26 @@ func (c *ClientConn) Read(b []byte) (int, error) { c.peerCache = c.peerCache[n:] return n, nil } - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { - return 0, err - } - peerLength, err := DecodeHeader(peerHeader) // 17~17000 + h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 if err != nil { - if c.instance != nil { - c.instance.Lock() - if bytes.Equal(c.ticket, c.instance.ticket) { - c.instance.expire = time.Now() // expired - } - c.instance.Unlock() - } return 0, err } - peerData := make([]byte, peerLength) + if t != 23 { + return 0, errors.New("unexpected type ", t, ", expect encrypted data") + } + peerData := make([]byte, l) if _, err := io.ReadFull(c.Conn, peerData); err != nil { return 0, err } - dst := peerData[:peerLength-16] + dst := peerData[:l-16] if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(ClientCipher, c.baseKey, peerData, peerHeader) + peerAead = NewAead(ClientCipher, c.baseKey, peerData, h) } - _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) + _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { c.peerAead = peerAead } diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 0cd23e16174a..2141f2d966fa 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -5,7 +5,9 @@ import ( "crypto/aes" "crypto/cipher" "crypto/sha256" - "strconv" + "fmt" + "io" + "net" "github.com/xtls/xray-core/common/errors" "golang.org/x/crypto/chacha20poly1305" @@ -14,23 +16,49 @@ import ( var MaxNonce = bytes.Repeat([]byte{255}, 12) -func EncodeHeader(b []byte, l int) { - b[0] = 23 - b[1] = 3 - b[2] = 3 - b[3] = byte(l >> 8) - b[4] = byte(l) +func EncodeHeader(h []byte, t byte, l int) { + switch t { + case 1: + h[0] = 1 + h[1] = 1 + h[2] = 1 + case 0: + h[0] = 0 + h[1] = 0 + h[2] = 0 + case 23: + h[0] = 23 + h[1] = 3 + h[2] = 3 + } + h[3] = byte(l >> 8) + h[4] = byte(l) } -func DecodeHeader(b []byte) (int, error) { - if b[0] == 23 && b[1] == 3 && b[2] == 3 { - l := int(b[3])<<8 | int(b[4]) - if l < 17 || l > 17000 { // TODO: TLSv1.3 max length - return 0, errors.New("invalid length in record's header: " + strconv.Itoa(l)) - } - return l, nil +func DecodeHeader(h []byte) (t byte, l int, err error) { + l = int(h[3])<<8 | int(h[4]) + if h[0] == 23 && h[1] == 3 && h[2] == 3 { + t = 23 + } else if h[0] == 0 && h[1] == 0 && h[2] == 0 { + t = 0 + } else if h[0] == 1 && h[1] == 1 && h[2] == 1 { + t = 1 + } else { + h = nil } - return 0, errors.New("invalid record's header") + if h == nil || l < 17 || l > 17000 { // TODO: TLSv1.3 max length + err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) + } + return +} + +func ReadAndDecodeHeader(conn net.Conn) (h []byte, t byte, l int, err error) { + h = make([]byte, 5) + if _, err = io.ReadFull(conn, h); err != nil { + return + } + t, l, err = DecodeHeader(h) + return } func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 1cf5b2f929e0..71aed4a21ae8 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -56,12 +56,12 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat go func() { for { time.Sleep(time.Minute) - now := time.Now() i.Lock() if i.closed { i.Unlock() return } + now := time.Now() for ticket, session := range i.sessions { if now.After(session.expire) { delete(i.sessions, ticket) @@ -90,40 +90,49 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { } c := &ServerConn{Conn: conn} - peerTicketHello := make([]byte, 21+32) - if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { + _, t, l, err := ReadAndDecodeHeader(c.Conn) + if err != nil { return nil, err } - if i.minutes > 0 { + if t == 23 { + return nil, errors.New("unexpected data") + } + + if t == 0 { + if i.minutes == 0 { + return nil, errors.New("0-RTT is not allowed") + } + peerTicketHello := make([]byte, 21+32) + if l != len(peerTicketHello) { + return nil, errors.New("unexpected length ", l, " for ticket hello") + } + if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { + return nil, err + } i.RLock() s := i.sessions[[21]byte(peerTicketHello)] i.RUnlock() - if s != nil { - if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[21:]), true); !replay { - c.cipher = s.cipher - c.baseKey = s.baseKey - c.ticket = peerTicketHello[:21] - c.peerRandom = peerTicketHello[21:] - return c, nil - } + if s == nil { + noise := make([]byte, crypto.RandBetween(100, 1000)) + rand.Read(noise) + c.Conn.Write(noise) // make client do new handshake + return nil, errors.New("expired ticket") } - } - - peerHeader := make([]byte, 5) - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { - return nil, err - } - if l, _ := DecodeHeader(peerHeader); l != 0 { - noise := make([]byte, crypto.RandBetween(100, 1000)) - rand.Read(noise) - c.Conn.Write(noise) // make client do new handshake - return nil, errors.New("invalid ticket") + if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[21:]), true); replay { + return nil, errors.New("replay detected") + } + c.cipher = s.cipher + c.baseKey = s.baseKey + c.ticket = peerTicketHello[:21] + c.peerRandom = peerTicketHello[21:] + return c, nil } peerClientHello := make([]byte, 1+1184+1088) - copy(peerClientHello, peerTicketHello) - copy(peerClientHello[53:], peerHeader) - if _, err := io.ReadFull(c.Conn, peerClientHello[58:]); err != nil { + if l != len(peerClientHello) { + return nil, errors.New("unexpected length ", l, " for client hello") + } + if _, err := io.ReadFull(c.Conn, peerClientHello); err != nil { return nil, err } c.cipher = peerClientHello[0] @@ -146,16 +155,17 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { paddingLen := crypto.RandBetween(100, 1000) - serverHello := make([]byte, 1088+21+5+paddingLen) - copy(serverHello, encapsulatedPfsKey) - copy(serverHello[1088:], c.ticket) - EncodeHeader(serverHello[1109:], int(paddingLen)) - rand.Read(serverHello[1114:]) + serverHello := make([]byte, 5+1088+21+5+paddingLen) + EncodeHeader(serverHello, 1, 1088+21) + copy(serverHello[5:], encapsulatedPfsKey) + copy(serverHello[5+1088:], c.ticket) + EncodeHeader(serverHello[5+1088+21:], 23, int(paddingLen)) + rand.Read(serverHello[5+1088+21+5:]) - if _, err := c.Conn.Write(serverHello); err != nil { + if n, err := c.Conn.Write(serverHello); n != len(serverHello) || err != nil { return nil, err } - // we can send more padding if needed + // server can send more padding / PFS AEAD messages if needed if i.minutes > 0 { i.Lock() @@ -174,24 +184,30 @@ func (c *ServerConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - peerHeader := make([]byte, 5) if c.peerAead == nil { - if c.peerRandom == nil { + if c.peerRandom == nil { // 1-RTT + var t byte + var l int + var err error for { - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + if _, t, l, err = ReadAndDecodeHeader(c.Conn); err != nil { return 0, err } - peerPaddingLen, _ := DecodeHeader(peerHeader) - if peerPaddingLen == 0 { + if t != 23 { break } - if _, err := io.ReadFull(c.Conn, make([]byte, peerPaddingLen)); err != nil { + if _, err := io.ReadFull(c.Conn, make([]byte, l)); err != nil { return 0, err } } + if t != 0 { + return 0, errors.New("unexpected type ", t, ", expect ticket hello") + } peerTicket := make([]byte, 21) - copy(peerTicket, peerHeader) - if _, err := io.ReadFull(c.Conn, peerTicket[5:]); err != nil { + if l != len(peerTicket) { + return 0, errors.New("unexpected length ", l, " for ticket hello") + } + if _, err := io.ReadFull(c.Conn, peerTicket); err != nil { return 0, err } if !bytes.Equal(peerTicket, c.ticket) { @@ -210,32 +226,32 @@ func (c *ServerConn) Read(b []byte) (int, error) { c.peerCache = c.peerCache[n:] return n, nil } - if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { - return 0, err - } - peerLength, err := DecodeHeader(peerHeader) // 17~17000 + h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 if err != nil { return 0, err } - peerData := make([]byte, peerLength) + if t != 23 { + return 0, errors.New("unexpected type ", t, ", expect encrypted data") + } + peerData := make([]byte, l) if _, err := io.ReadFull(c.Conn, peerData); err != nil { return 0, err } - dst := peerData[:peerLength-16] + dst := peerData[:l-16] if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(c.cipher, c.baseKey, peerData, peerHeader) + peerAead = NewAead(c.cipher, c.baseKey, peerData, h) } - _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, peerHeader) + _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { c.peerAead = peerAead } IncreaseNonce(c.peerNonce) if err != nil { - return 0, errors.New("error") + return 0, err } if len(dst) > len(b) { c.peerCache = dst[copy(b, dst):] @@ -259,22 +275,23 @@ func (c *ServerConn) Write(b []byte) (int, error) { if c.peerRandom == nil { return 0, errors.New("empty c.peerRandom") } - data = make([]byte, 32+5+len(b)+16) - rand.Read(data[:32]) - c.aead = NewAead(c.cipher, c.baseKey, data[:32], c.peerRandom) + data = make([]byte, 5+32+5+len(b)+16) + EncodeHeader(data, 0, 32) + rand.Read(data[5 : 5+32]) + c.aead = NewAead(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) c.nonce = make([]byte, 12) - EncodeHeader(data[32:], len(b)+16) - c.aead.Seal(data[:37], c.nonce, b, data[32:37]) + EncodeHeader(data[5+32:], 23, len(b)+16) + c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5]) } else { data = make([]byte, 5+len(b)+16) - EncodeHeader(data, len(b)+16) + EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { c.aead = NewAead(c.cipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { + if n, err := c.Conn.Write(data); n != len(data) || err != nil { return 0, err } } From 1720be168fa069332c418503d30341fc6e01df7f Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:41:26 +0000 Subject: [PATCH 10/25] aes128xor (all) -> xored (optimized) https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3182301703 --- infra/conf/vless.go | 4 ++-- proxy/vless/encryption/xor.go | 43 +++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 301e28bf54e9..a13b735be3f5 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -92,7 +92,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { } switch s[1] { case "vless": - case "aes128xor": + case "xored": config.Xor = 1 default: return false @@ -240,7 +240,7 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } switch s[1] { case "vless": - case "aes128xor": + case "xored": account.Xor = 1 default: return false diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index 296fdd637af2..696702bc69df 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -10,16 +10,19 @@ import ( type XorConn struct { net.Conn - key []byte - ctr cipher.Stream - peerCtr cipher.Stream + key []byte + ctr cipher.Stream + peerCtr cipher.Stream + isHeader bool + skipNext bool } func NewXorConn(conn net.Conn, key []byte) *XorConn { return &XorConn{Conn: conn, key: key[:16]} + //chacha20.NewUnauthenticatedCipher() } -func (c *XorConn) Write(b []byte) (int, error) { +func (c *XorConn) Write(b []byte) (int, error) { // two records at most if len(b) == 0 { return 0, nil } @@ -30,7 +33,13 @@ func (c *XorConn) Write(b []byte) (int, error) { rand.Read(iv) c.ctr = cipher.NewCTR(block, iv) } - c.ctr.XORKeyStream(b, b) // caller MUST discard b + t, l, _ := DecodeHeader(b) + if t != 23 { + l += 10 // 5+l+5 + } else { + l = 5 + } + c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b if iv != nil { b = append(iv, b...) } @@ -43,7 +52,7 @@ func (c *XorConn) Write(b []byte) (int, error) { return len(b), nil } -func (c *XorConn) Read(b []byte) (int, error) { +func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... if len(b) == 0 { return 0, nil } @@ -54,10 +63,24 @@ func (c *XorConn) Read(b []byte) (int, error) { } block, _ := aes.NewCipher(c.key) c.peerCtr = cipher.NewCTR(block, peerIv) + c.isHeader = true + } + if _, err := io.ReadFull(c.Conn, b); err != nil { + return 0, err } - n, err := c.Conn.Read(b) - if n > 0 { - c.peerCtr.XORKeyStream(b[:n], b[:n]) + if c.skipNext { + c.skipNext = false + return len(b), nil } - return n, err + c.peerCtr.XORKeyStream(b, b) + if c.isHeader { + if t, _, _ := DecodeHeader(b); t == 23 { // always 5-bytes + c.skipNext = true + } else { + c.isHeader = false + } + } else { + c.isHeader = true + } + return len(b), nil } From 0fd7691d6b28e05922d7a5a9313d97745a51ea63 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:24:10 +0000 Subject: [PATCH 11/25] Fix reading ticket hello https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3183283514 https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3183324745 --- proxy/vless/encryption/client.go | 12 ++++++------ proxy/vless/encryption/server.go | 13 +++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 52b4828b0267..ecccdca032dc 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -212,19 +212,19 @@ func (c *ClientConn) Read(b []byte) (int, error) { } } if t != 0 { - return 0, errors.New("unexpected type ", t, ", expect server random") + return 0, errors.New("unexpected type ", t, ", expect random hello") } - peerRandom := make([]byte, 32) - if l != len(peerRandom) { - return 0, errors.New("unexpected length ", l, " for server random") + peerRandomHello := make([]byte, 32) + if l != len(peerRandomHello) { + return 0, errors.New("unexpected length ", l, " for random hello") } - if _, err := io.ReadFull(c.Conn, peerRandom); err != nil { + if _, err := io.ReadFull(c.Conn, peerRandomHello); err != nil { return 0, err } if c.random == nil { return 0, errors.New("empty c.random") } - c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandom, c.random) + c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandomHello, c.random) c.peerNonce = make([]byte, 12) } if len(c.peerCache) != 0 { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 71aed4a21ae8..4765ce0e2659 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -203,20 +203,17 @@ func (c *ServerConn) Read(b []byte) (int, error) { if t != 0 { return 0, errors.New("unexpected type ", t, ", expect ticket hello") } - peerTicket := make([]byte, 21) - if l != len(peerTicket) { + peerTicketHello := make([]byte, 21+32) + if l != len(peerTicketHello) { return 0, errors.New("unexpected length ", l, " for ticket hello") } - if _, err := io.ReadFull(c.Conn, peerTicket); err != nil { + if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { return 0, err } - if !bytes.Equal(peerTicket, c.ticket) { + if !bytes.Equal(peerTicketHello[:21], c.ticket) { return 0, errors.New("naughty boy") } - c.peerRandom = make([]byte, 32) - if _, err := io.ReadFull(c.Conn, c.peerRandom); err != nil { - return 0, err - } + c.peerRandom = peerTicketHello[21:] } c.peerAead = NewAead(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) From 09cc92c61d9067e0d65c1cae9124664ecfc78f43 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:44:58 +0000 Subject: [PATCH 12/25] chore https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3183681018 --- proxy/vless/encryption/client.go | 6 +++--- proxy/vless/encryption/server.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index ecccdca032dc..a51f2930c46a 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -93,7 +93,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { EncodeHeader(clientHello[5+1+1184+1088:], 23, int(paddingLen)) rand.Read(clientHello[5+1+1184+1088+5:]) - if n, err := c.Conn.Write(clientHello); n != len(clientHello) || err != nil { + if _, err := c.Conn.Write(clientHello); err != nil { return nil, err } // client can send more padding / NFS AEAD messages if needed @@ -170,7 +170,7 @@ func (c *ClientConn) Write(b []byte) (int, error) { } } IncreaseNonce(c.nonce) - if n, err := c.Conn.Write(data); n != len(data) || err != nil { + if _, err := c.Conn.Write(data); err != nil { return 0, err } } @@ -185,7 +185,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { var t byte var l int var err error - if c.instance == nil { // 1-RTT + if c.instance == nil { // from 1-RTT for { if _, t, l, err = ReadAndDecodeHeader(c.Conn); err != nil { return 0, err diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 4765ce0e2659..99b73ce4ee7f 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -162,7 +162,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { EncodeHeader(serverHello[5+1088+21:], 23, int(paddingLen)) rand.Read(serverHello[5+1088+21+5:]) - if n, err := c.Conn.Write(serverHello); n != len(serverHello) || err != nil { + if _, err := c.Conn.Write(serverHello); err != nil { return nil, err } // server can send more padding / PFS AEAD messages if needed @@ -185,7 +185,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAead == nil { - if c.peerRandom == nil { // 1-RTT + if c.peerRandom == nil { // from 1-RTT var t byte var l int var err error @@ -288,7 +288,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { } } IncreaseNonce(c.nonce) - if n, err := c.Conn.Write(data); n != len(data) || err != nil { + if _, err := c.Conn.Write(data); err != nil { return 0, err } } From 7f778a4e2f123dc03fe57fbf24da59dcaf270f8a Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:37:06 +0000 Subject: [PATCH 13/25] SHA256(nfsEKeyBytes) for XOR's key https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3185590465 --- proxy/vless/encryption/client.go | 19 ++++++++++--------- proxy/vless/encryption/server.go | 17 +++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index a51f2930c46a..2faf745acdd7 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -5,6 +5,7 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" + "crypto/sha256" "io" "net" "sync" @@ -25,13 +26,13 @@ func init() { type ClientInstance struct { sync.RWMutex - nfsEKey *mlkem.EncapsulationKey768 - nfsEKeyBytes []byte - xor uint32 - minutes time.Duration - expire time.Time - baseKey []byte - ticket []byte + nfsEKey *mlkem.EncapsulationKey768 + nfsEKeySha256 [32]byte + xor uint32 + minutes time.Duration + expire time.Time + baseKey []byte + ticket []byte } type ClientConn struct { @@ -50,7 +51,7 @@ type ClientConn struct { func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes) if xor > 0 { - i.nfsEKeyBytes = nfsEKeyBytes + i.nfsEKeySha256 = sha256.Sum256(nfsEKeyBytes) i.xor = xor } i.minutes = minutes @@ -62,7 +63,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { return nil, errors.New("uninitialized") } if i.xor > 0 { - conn = NewXorConn(conn, i.nfsEKeyBytes) + conn = NewXorConn(conn, i.nfsEKeySha256[:]) } c := &ClientConn{Conn: conn} diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 99b73ce4ee7f..49e0b9df6e4e 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -5,6 +5,7 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" + "crypto/sha256" "io" "net" "sync" @@ -23,12 +24,12 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex - nfsDKey *mlkem.DecapsulationKey768 - nfsEKeyBytes []byte - xor uint32 - minutes time.Duration - sessions map[[21]byte]*ServerSession - closed bool + nfsDKey *mlkem.DecapsulationKey768 + nfsEKeySha256 [32]byte + xor uint32 + minutes time.Duration + sessions map[[21]byte]*ServerSession + closed bool } type ServerConn struct { @@ -47,7 +48,7 @@ type ServerConn struct { func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Duration) (err error) { i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed) if xor > 0 { - i.nfsEKeyBytes = i.nfsDKey.EncapsulationKey().Bytes() + i.nfsEKeySha256 = sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) i.xor = xor } if minutes > 0 { @@ -86,7 +87,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { return nil, errors.New("uninitialized") } if i.xor > 0 { - conn = NewXorConn(conn, i.nfsEKeyBytes) + conn = NewXorConn(conn, i.nfsEKeySha256[:]) } c := &ServerConn{Conn: conn} From 2807ee432a1fbeb301815647189eacd650b12a8b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:28:17 +0000 Subject: [PATCH 14/25] Allow paddings before handshake; CTR 128->256; Fix panic https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3187832651 --- proxy/vless/encryption/client.go | 58 +++++++++++++------------------- proxy/vless/encryption/common.go | 17 ++++++++-- proxy/vless/encryption/server.go | 47 +++++++++++--------------- proxy/vless/encryption/xor.go | 2 +- 4 files changed, 59 insertions(+), 65 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 2faf745acdd7..1ea2ecc51370 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "io" "net" + "strings" "sync" "time" @@ -26,13 +27,12 @@ func init() { type ClientInstance struct { sync.RWMutex - nfsEKey *mlkem.EncapsulationKey768 - nfsEKeySha256 [32]byte - xor uint32 - minutes time.Duration - expire time.Time - baseKey []byte - ticket []byte + nfsEKey *mlkem.EncapsulationKey768 + xorKey []byte + minutes time.Duration + expire time.Time + baseKey []byte + ticket []byte } type ClientConn struct { @@ -49,10 +49,17 @@ type ClientConn struct { } func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { + if i.nfsEKey != nil { + err = errors.New("already initialized") + return + } i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes) + if err != nil { + return + } if xor > 0 { - i.nfsEKeySha256 = sha256.Sum256(nfsEKeyBytes) - i.xor = xor + xorKey := sha256.Sum256(nfsEKeyBytes) + i.xorKey = xorKey[:] } i.minutes = minutes return @@ -62,8 +69,8 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.nfsEKey == nil { return nil, errors.New("uninitialized") } - if i.xor > 0 { - conn = NewXorConn(conn, i.nfsEKeySha256[:]) + if i.xorKey != nil { + conn = NewXorConn(conn, i.xorKey) } c := &ClientConn{Conn: conn} @@ -99,14 +106,14 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { } // client can send more padding / NFS AEAD messages if needed - _, t, l, err := ReadAndDecodeHeader(c.Conn) + _, t, l, err := ReadAndDiscardPaddings(c.Conn) if err != nil { return nil, err } + if t != 1 { return nil, errors.New("unexpected type ", t, ", expect server hello") } - peerServerHello := make([]byte, 1088+21) if l != len(peerServerHello) { return nil, errors.New("unexpected length ", l, " for server hello") @@ -183,27 +190,9 @@ func (c *ClientConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAead == nil { - var t byte - var l int - var err error - if c.instance == nil { // from 1-RTT - for { - if _, t, l, err = ReadAndDecodeHeader(c.Conn); err != nil { - return 0, err - } - if t != 23 { - break - } - if _, err := io.ReadFull(c.Conn, make([]byte, l)); err != nil { - return 0, err - } - } - } else { - h := make([]byte, 5) - if _, err := io.ReadFull(c.Conn, h); err != nil { - return 0, err - } - if t, l, err = DecodeHeader(h); err != nil { + _, t, l, err := ReadAndDiscardPaddings(c.Conn) + if err != nil { + if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // from 0-RTT c.instance.Lock() if bytes.Equal(c.ticket, c.instance.ticket) { c.instance.expire = time.Now() // expired @@ -211,6 +200,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { c.instance.Unlock() return 0, errors.New("new handshake needed") } + return 0, err } if t != 0 { return 0, errors.New("unexpected type ", t, ", expect random hello") diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 2141f2d966fa..7f879dc17f96 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -44,10 +44,10 @@ func DecodeHeader(h []byte) (t byte, l int, err error) { } else if h[0] == 1 && h[1] == 1 && h[2] == 1 { t = 1 } else { - h = nil + l = 0 } - if h == nil || l < 17 || l > 17000 { // TODO: TLSv1.3 max length - err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) + if l < 17 || l > 17000 { // TODO: TLSv1.3 max length + err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) // relied by client's Read() } return } @@ -61,6 +61,17 @@ func ReadAndDecodeHeader(conn net.Conn) (h []byte, t byte, l int, err error) { return } +func ReadAndDiscardPaddings(conn net.Conn) (h []byte, t byte, l int, err error) { + for { + if h, t, l, err = ReadAndDecodeHeader(conn); err != nil || t != 23 { + return + } + if _, err = io.ReadFull(conn, make([]byte, l)); err != nil { + return + } + } +} + func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { key := make([]byte, 32) hkdf.New(sha256.New, secret, salt, info).Read(key) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 49e0b9df6e4e..72346575061d 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -24,12 +24,11 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex - nfsDKey *mlkem.DecapsulationKey768 - nfsEKeySha256 [32]byte - xor uint32 - minutes time.Duration - sessions map[[21]byte]*ServerSession - closed bool + nfsDKey *mlkem.DecapsulationKey768 + xorKey []byte + minutes time.Duration + sessions map[[21]byte]*ServerSession + closed bool } type ServerConn struct { @@ -46,10 +45,17 @@ type ServerConn struct { } func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Duration) (err error) { + if i.nfsDKey != nil { + err = errors.New("already initialized") + return + } i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed) + if err != nil { + return + } if xor > 0 { - i.nfsEKeySha256 = sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - i.xor = xor + xorKey := sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + i.xorKey = xorKey[:] } if minutes > 0 { i.minutes = minutes @@ -86,18 +92,15 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.nfsDKey == nil { return nil, errors.New("uninitialized") } - if i.xor > 0 { - conn = NewXorConn(conn, i.nfsEKeySha256[:]) + if i.xorKey != nil { + conn = NewXorConn(conn, i.xorKey) } c := &ServerConn{Conn: conn} - _, t, l, err := ReadAndDecodeHeader(c.Conn) + _, t, l, err := ReadAndDiscardPaddings(c.Conn) if err != nil { return nil, err } - if t == 23 { - return nil, errors.New("unexpected data") - } if t == 0 { if i.minutes == 0 { @@ -187,19 +190,9 @@ func (c *ServerConn) Read(b []byte) (int, error) { } if c.peerAead == nil { if c.peerRandom == nil { // from 1-RTT - var t byte - var l int - var err error - for { - if _, t, l, err = ReadAndDecodeHeader(c.Conn); err != nil { - return 0, err - } - if t != 23 { - break - } - if _, err := io.ReadFull(c.Conn, make([]byte, l)); err != nil { - return 0, err - } + _, t, l, err := ReadAndDiscardPaddings(c.Conn) + if err != nil { + return 0, err } if t != 0 { return 0, errors.New("unexpected type ", t, ", expect ticket hello") diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index 696702bc69df..bbe489ef0875 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -18,7 +18,7 @@ type XorConn struct { } func NewXorConn(conn net.Conn, key []byte) *XorConn { - return &XorConn{Conn: conn, key: key[:16]} + return &XorConn{Conn: conn, key: key} //chacha20.NewUnauthenticatedCipher() } From bfe4820f2f086daf639b1957eb23dc13c843cad1 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:28:40 +0000 Subject: [PATCH 15/25] Fix 1/67000000 chance's server panic; Refine comments https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3188118918 --- proxy/vless/encryption/client.go | 8 ++++---- proxy/vless/encryption/common.go | 2 +- proxy/vless/encryption/server.go | 18 +++++++++++------- proxy/vless/encryption/xor.go | 12 ++++++------ 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 1ea2ecc51370..9a17135eac39 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -104,9 +104,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if _, err := c.Conn.Write(clientHello); err != nil { return nil, err } - // client can send more padding / NFS AEAD messages if needed + // client can send more paddings / NFS AEAD messages if needed - _, t, l, err := ReadAndDiscardPaddings(c.Conn) + _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before server hello if err != nil { return nil, err } @@ -190,9 +190,9 @@ func (c *ClientConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAead == nil { - _, t, l, err := ReadAndDiscardPaddings(c.Conn) + _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before random hello if err != nil { - if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // from 0-RTT + if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT's 0-RTT c.instance.Lock() if bytes.Equal(c.ticket, c.instance.ticket) { c.instance.expire = time.Now() // expired diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 7f879dc17f96..58c096e3f2b4 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -47,7 +47,7 @@ func DecodeHeader(h []byte) (t byte, l int, err error) { l = 0 } if l < 17 || l > 17000 { // TODO: TLSv1.3 max length - err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) // relied by client's Read() + 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 index 72346575061d..c5b0163e812a 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -97,7 +97,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { } c := &ServerConn{Conn: conn} - _, t, l, err := ReadAndDiscardPaddings(c.Conn) + _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before client/ticket hello if err != nil { return nil, err } @@ -117,9 +117,13 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { s := i.sessions[[21]byte(peerTicketHello)] i.RUnlock() if s == nil { - noise := make([]byte, crypto.RandBetween(100, 1000)) - rand.Read(noise) - c.Conn.Write(noise) // make client do new handshake + noises := make([]byte, crypto.RandBetween(100, 1000)) + var err error + for err == nil { + rand.Read(noises) + _, _, err = DecodeHeader(noises) + } + c.Conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[21:]), true); replay { @@ -169,7 +173,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if _, err := c.Conn.Write(serverHello); err != nil { return nil, err } - // server can send more padding / PFS AEAD messages if needed + // server can send more paddings / PFS AEAD messages if needed if i.minutes > 0 { i.Lock() @@ -189,8 +193,8 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAead == nil { - if c.peerRandom == nil { // from 1-RTT - _, t, l, err := ReadAndDiscardPaddings(c.Conn) + if c.peerRandom == nil { // 1-RTT's 0-RTT + _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before ticket hello if err != nil { return 0, err } diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index bbe489ef0875..64828eaa26ab 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -22,7 +22,7 @@ func NewXorConn(conn net.Conn, key []byte) *XorConn { //chacha20.NewUnauthenticatedCipher() } -func (c *XorConn) Write(b []byte) (int, error) { // two records at most +func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records if len(b) == 0 { return 0, nil } @@ -34,10 +34,10 @@ func (c *XorConn) Write(b []byte) (int, error) { // two records at most c.ctr = cipher.NewCTR(block, iv) } t, l, _ := DecodeHeader(b) - if t != 23 { - l += 10 // 5+l+5 - } else { + if t == 23 { // single 23 l = 5 + } else { // 1/0 + 23, or noises only + l += 10 } c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b if iv != nil { @@ -73,8 +73,8 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... return len(b), nil } c.peerCtr.XORKeyStream(b, b) - if c.isHeader { - if t, _, _ := DecodeHeader(b); t == 23 { // always 5-bytes + if c.isHeader { // always 5-bytes + if t, _, _ := DecodeHeader(b); t == 23 { c.skipNext = true } else { c.isHeader = false From d1fb48521271251a8c74bd64fcc2fc8700717a3b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:39:53 +0000 Subject: [PATCH 16/25] Add hash11(nfsEKeyBytes) to client/ticket hello; Support XTLS Vision for native appearance https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3194609798 --- infra/conf/vless.go | 14 ++++---- proxy/vless/encoding/encoding.go | 24 ++++++++------ proxy/vless/encryption/client.go | 45 ++++++++++++++------------ proxy/vless/encryption/server.go | 55 +++++++++++++++++++------------- proxy/vless/inbound/inbound.go | 7 +++- proxy/vless/outbound/outbound.go | 7 +++- 6 files changed, 90 insertions(+), 62 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index a13b735be3f5..34288d163989 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -56,15 +56,15 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { account.Id = u.String() switch account.Flow { - case "": - case vless.XRV: - if c.Decryption != "none" { - return nil, errors.New(`VLESS clients: "decryption" doesn't support "flow" yet`) - } + case "", vless.XRV: default: return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } + if strings.Contains(c.Decryption, "xored") && account.Flow == vless.XRV { + return nil, errors.New(`VLESS clients: "xored" doesn't support "flow" yet`) + } + if account.Encryption != "" { return nil, errors.New(`VLESS clients: "encryption" should not in inbound settings`) } @@ -215,8 +215,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { switch account.Flow { case "": case vless.XRV, vless.XRV + "-udp443": - if account.Encryption != "none" { - return nil, errors.New(`VLESS users: "encryption" doesn't support "flow" yet`) + if strings.Contains(account.Encryption, "xored") { + return nil, errors.New(`VLESS users: "xored" doesn't support "flow" yet`) } default: return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go index 38043e683c39..8246aef2450c 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 index 9a17135eac39..d0d4b3d56308 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -28,6 +28,7 @@ func init() { type ClientInstance struct { sync.RWMutex nfsEKey *mlkem.EncapsulationKey768 + hash11 [11]byte // no more capacity xorKey []byte minutes time.Duration expire time.Time @@ -45,7 +46,7 @@ type ClientConn struct { nonce []byte peerAead cipher.AEAD peerNonce []byte - peerCache []byte + PeerCache []byte } func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { @@ -57,6 +58,8 @@ func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Dura if err != nil { return } + hash256 := sha256.Sum256(nfsEKeyBytes) + copy(i.hash11[:], hash256[:]) if xor > 0 { xorKey := sha256.Sum256(nfsEKeyBytes) i.xorKey = xorKey[:] @@ -93,13 +96,14 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate() paddingLen := crypto.RandBetween(100, 1000) - clientHello := make([]byte, 5+1+1184+1088+5+paddingLen) - EncodeHeader(clientHello, 1, 1+1184+1088) - clientHello[5] = ClientCipher - copy(clientHello[5+1:], pfsEKeyBytes) - copy(clientHello[5+1+1184:], encapsulatedNfsKey) - EncodeHeader(clientHello[5+1+1184+1088:], 23, int(paddingLen)) - rand.Read(clientHello[5+1+1184+1088+5:]) + clientHello := make([]byte, 5+11+1+1184+1088+5+paddingLen) + EncodeHeader(clientHello, 1, 11+1+1184+1088) + copy(clientHello[5:], i.hash11[:]) + clientHello[5+11] = ClientCipher + copy(clientHello[5+11+1:], pfsEKeyBytes) + copy(clientHello[5+11+1+1184:], encapsulatedNfsKey) + EncodeHeader(clientHello[5+11+1+1184+1088:], 23, int(paddingLen)) + rand.Read(clientHello[5+11+1+1184+1088+5:]) if _, err := c.Conn.Write(clientHello); err != nil { return nil, err @@ -122,7 +126,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { return nil, err } encapsulatedPfsKey := peerServerHello[:1088] - c.ticket = peerServerHello[1088:] + c.ticket = append(i.hash11[:], peerServerHello[1088:]...) pfsKey, err := pfsDKey.Decapsulate(encapsulatedPfsKey) if err != nil { @@ -130,8 +134,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { } c.baseKey = append(pfsKey, nfsKey...) - nonce := [12]byte{ClientCipher} - VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, nonce[:], c.ticket, pfsEKeyBytes) + VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) if !bytes.Equal(VLESS, []byte("VLESS")) { return nil, errors.New("invalid server").AtError() } @@ -159,16 +162,16 @@ func (c *ClientConn) Write(b []byte) (int, error) { } n += len(b) if c.aead == nil { + data = make([]byte, 5+32+32+5+len(b)+16) + EncodeHeader(data, 0, 32+32) + copy(data[5:], c.ticket) c.random = make([]byte, 32) rand.Read(c.random) + copy(data[5+32:], c.random) + EncodeHeader(data[5+32+32:], 23, len(b)+16) c.aead = NewAead(ClientCipher, c.baseKey, c.random, c.ticket) c.nonce = make([]byte, 12) - data = make([]byte, 5+21+32+5+len(b)+16) - EncodeHeader(data, 0, 21+32) - copy(data[5:], c.ticket) - copy(data[5+21:], c.random) - EncodeHeader(data[5+21+32:], 23, len(b)+16) - c.aead.Seal(data[:5+21+32+5], c.nonce, b, data[5+21+32:5+21+32+5]) + c.aead.Seal(data[:5+32+32+5], c.nonce, b, data[5+32+32:5+32+32+5]) } else { data = make([]byte, 5+len(b)+16) EncodeHeader(data, 23, len(b)+16) @@ -218,9 +221,9 @@ func (c *ClientConn) Read(b []byte) (int, error) { c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandomHello, c.random) c.peerNonce = make([]byte, 12) } - if len(c.peerCache) != 0 { - n := copy(b, c.peerCache) - c.peerCache = c.peerCache[n:] + if len(c.PeerCache) != 0 { + n := copy(b, c.PeerCache) + c.PeerCache = c.PeerCache[n:] return n, nil } h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 @@ -251,7 +254,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { return 0, err } if len(dst) > len(b) { - c.peerCache = dst[copy(b, dst):] + c.PeerCache = dst[copy(b, dst):] dst = b // for len(dst) } return len(dst), nil diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index c5b0163e812a..5ea1f0d6210f 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -6,6 +6,7 @@ import ( "crypto/mlkem" "crypto/rand" "crypto/sha256" + "fmt" "io" "net" "sync" @@ -25,9 +26,10 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex nfsDKey *mlkem.DecapsulationKey768 + hash11 [11]byte // no more capacity xorKey []byte minutes time.Duration - sessions map[[21]byte]*ServerSession + sessions map[[32]byte]*ServerSession closed bool } @@ -39,7 +41,7 @@ type ServerConn struct { peerRandom []byte peerAead cipher.AEAD peerNonce []byte - peerCache []byte + PeerCache []byte aead cipher.AEAD nonce []byte } @@ -53,13 +55,15 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat if err != nil { return } + hash256 := sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + copy(i.hash11[:], hash256[:]) if xor > 0 { xorKey := sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) i.xorKey = xorKey[:] } if minutes > 0 { i.minutes = minutes - i.sessions = make(map[[21]byte]*ServerSession) + i.sessions = make(map[[32]byte]*ServerSession) go func() { for { time.Sleep(time.Minute) @@ -106,15 +110,18 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.minutes == 0 { return nil, errors.New("0-RTT is not allowed") } - peerTicketHello := make([]byte, 21+32) + peerTicketHello := make([]byte, 32+32) if l != len(peerTicketHello) { return nil, errors.New("unexpected length ", l, " for ticket hello") } if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { return nil, err } + if !bytes.Equal(peerTicketHello[:11], i.hash11[:]) { + return nil, errors.New("unexpected hash11: ", fmt.Sprintf("%v", peerTicketHello[:11])) + } i.RLock() - s := i.sessions[[21]byte(peerTicketHello)] + s := i.sessions[[32]byte(peerTicketHello)] i.RUnlock() if s == nil { noises := make([]byte, crypto.RandBetween(100, 1000)) @@ -126,26 +133,29 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { c.Conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } - if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[21:]), true); replay { + if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[32:]), true); replay { return nil, errors.New("replay detected") } c.cipher = s.cipher c.baseKey = s.baseKey - c.ticket = peerTicketHello[:21] - c.peerRandom = peerTicketHello[21:] + c.ticket = peerTicketHello[:32] + c.peerRandom = peerTicketHello[32:] return c, nil } - peerClientHello := make([]byte, 1+1184+1088) + peerClientHello := make([]byte, 11+1+1184+1088) if l != len(peerClientHello) { return nil, errors.New("unexpected length ", l, " for client hello") } if _, err := io.ReadFull(c.Conn, peerClientHello); err != nil { return nil, err } - c.cipher = peerClientHello[0] - pfsEKeyBytes := peerClientHello[1:1185] - encapsulatedNfsKey := peerClientHello[1185:2273] + if !bytes.Equal(peerClientHello[:11], i.hash11[:]) { + return nil, errors.New("unexpected hash11: ", fmt.Sprintf("%v", peerClientHello[:11])) + } + c.cipher = peerClientHello[11] + pfsEKeyBytes := peerClientHello[11+1 : 11+1+1184] + encapsulatedNfsKey := peerClientHello[11+1+1184:] pfsEKey, err := mlkem.NewEncapsulationKey768(pfsEKeyBytes) if err != nil { @@ -158,15 +168,14 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) - nonce := [12]byte{c.cipher} - c.ticket = NewAead(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, nonce[:], []byte("VLESS"), pfsEKeyBytes) + c.ticket = append(i.hash11[:], NewAead(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) paddingLen := crypto.RandBetween(100, 1000) serverHello := make([]byte, 5+1088+21+5+paddingLen) EncodeHeader(serverHello, 1, 1088+21) copy(serverHello[5:], encapsulatedPfsKey) - copy(serverHello[5+1088:], c.ticket) + copy(serverHello[5+1088:], c.ticket[11:]) EncodeHeader(serverHello[5+1088+21:], 23, int(paddingLen)) rand.Read(serverHello[5+1088+21+5:]) @@ -177,7 +186,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.minutes > 0 { i.Lock() - i.sessions[[21]byte(c.ticket)] = &ServerSession{ + i.sessions[[32]byte(c.ticket)] = &ServerSession{ expire: time.Now().Add(i.minutes), cipher: c.cipher, baseKey: c.baseKey, @@ -201,24 +210,24 @@ func (c *ServerConn) Read(b []byte) (int, error) { if t != 0 { return 0, errors.New("unexpected type ", t, ", expect ticket hello") } - peerTicketHello := make([]byte, 21+32) + peerTicketHello := make([]byte, 32+32) if l != len(peerTicketHello) { return 0, errors.New("unexpected length ", l, " for ticket hello") } if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { return 0, err } - if !bytes.Equal(peerTicketHello[:21], c.ticket) { + if !bytes.Equal(peerTicketHello[:32], c.ticket) { return 0, errors.New("naughty boy") } - c.peerRandom = peerTicketHello[21:] + c.peerRandom = peerTicketHello[32:] } c.peerAead = NewAead(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } - if len(c.peerCache) != 0 { - n := copy(b, c.peerCache) - c.peerCache = c.peerCache[n:] + if len(c.PeerCache) != 0 { + n := copy(b, c.PeerCache) + c.PeerCache = c.PeerCache[n:] return n, nil } h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 @@ -249,7 +258,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, err } if len(dst) > len(b) { - c.peerCache = dst[copy(b, dst):] + c.PeerCache = dst[copy(b, dst):] dst = b // for len(dst) } return len(dst), nil diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 06c9e518f24c..deaf1cf18f0d 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -484,6 +484,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 { @@ -496,6 +497,10 @@ 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.ServerConn); ok { + peerCache = &serverConn.PeerCache + break + } var t reflect.Type var p uintptr if tlsConn, ok := iConn.(*tls.Conn); ok { @@ -564,7 +569,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 1bbd879b89d6..9be880390ba9 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -140,6 +140,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 @@ -158,6 +159,10 @@ 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.ClientConn); ok { + peerCache = &clientConn.PeerCache + break + } var t reflect.Type var p uintptr if tlsConn, ok := iConn.(*tls.Conn); ok { @@ -292,7 +297,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 49580705f6029648399304b816a2737f991582a8 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:58:21 +0000 Subject: [PATCH 17/25] Use SHA3-256 instead of SHA2-256; Support XTLS Vision for random appearance https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3197163816 --- infra/conf/vless.go | 10 +-- proxy/proxy.go | 4 + proxy/vless/encryption/client.go | 6 +- proxy/vless/encryption/common.go | 7 +- proxy/vless/encryption/server.go | 8 +- proxy/vless/encryption/xor.go | 138 ++++++++++++++++++++++--------- proxy/vless/inbound/inbound.go | 16 ++-- proxy/vless/outbound/outbound.go | 14 ++-- 8 files changed, 129 insertions(+), 74 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 34288d163989..5fcdf34c3801 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -61,10 +61,6 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } - if strings.Contains(c.Decryption, "xored") && account.Flow == vless.XRV { - return nil, errors.New(`VLESS clients: "xored" doesn't support "flow" yet`) - } - if account.Encryption != "" { return nil, errors.New(`VLESS clients: "encryption" should not in inbound settings`) } @@ -213,11 +209,7 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { account.Id = u.String() switch account.Flow { - case "": - case vless.XRV, vless.XRV + "-udp443": - if strings.Contains(account.Encryption, "xored") { - return nil, errors.New(`VLESS users: "xored" doesn't support "flow" yet`) - } + case "", vless.XRV, vless.XRV + "-udp443": default: return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 3fec31af94a9..38f38b41ea7c 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" @@ -534,6 +535,9 @@ func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { readCounter = statConn.ReadCounter writerCounter = statConn.WriteCounter } + if _, ok := conn.(*encryption.XorConn); ok { + return conn, readCounter, writerCounter + } if xc, ok := conn.(*tls.Conn); ok { conn = xc.NetConn() } else if utlsConn, ok := conn.(*tls.UConn); ok { diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index d0d4b3d56308..5092de5f0ea1 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -5,7 +5,7 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" - "crypto/sha256" + "crypto/sha3" "io" "net" "strings" @@ -58,10 +58,10 @@ func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Dura if err != nil { return } - hash256 := sha256.Sum256(nfsEKeyBytes) + hash256 := sha3.Sum256(nfsEKeyBytes) copy(i.hash11[:], hash256[:]) if xor > 0 { - xorKey := sha256.Sum256(nfsEKeyBytes) + xorKey := sha3.Sum256(nfsEKeyBytes) i.xorKey = xorKey[:] } i.minutes = minutes diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 58c096e3f2b4..d4484bd6787b 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -4,14 +4,14 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/sha256" + "crypto/hkdf" + "crypto/sha3" "fmt" "io" "net" "github.com/xtls/xray-core/common/errors" "golang.org/x/crypto/chacha20poly1305" - "golang.org/x/crypto/hkdf" ) var MaxNonce = bytes.Repeat([]byte{255}, 12) @@ -73,8 +73,7 @@ func ReadAndDiscardPaddings(conn net.Conn) (h []byte, t byte, l int, err error) } func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { - key := make([]byte, 32) - hkdf.New(sha256.New, secret, salt, info).Read(key) + key, _ := hkdf.Key(sha3.New256, secret, salt, string(info), 32) if c&1 == 1 { block, _ := aes.NewCipher(key) aead, _ = cipher.NewGCM(block) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 5ea1f0d6210f..c1d996110f34 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -5,7 +5,7 @@ import ( "crypto/cipher" "crypto/mlkem" "crypto/rand" - "crypto/sha256" + "crypto/sha3" "fmt" "io" "net" @@ -55,10 +55,10 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat if err != nil { return } - hash256 := sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + hash256 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) copy(i.hash11[:], hash256[:]) if xor > 0 { - xorKey := sha256.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + xorKey := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) i.xorKey = xorKey[:] } if minutes > 0 { @@ -282,9 +282,9 @@ func (c *ServerConn) Write(b []byte) (int, error) { data = make([]byte, 5+32+5+len(b)+16) EncodeHeader(data, 0, 32) rand.Read(data[5 : 5+32]) + EncodeHeader(data[5+32:], 23, len(b)+16) c.aead = NewAead(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) c.nonce = make([]byte, 12) - EncodeHeader(data[5+32:], 23, len(b)+16) c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5]) } else { data = make([]byte, 5+len(b)+16) diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index 64828eaa26ab..69ff15780ffb 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -15,6 +15,14 @@ type XorConn struct { peerCtr cipher.Stream isHeader bool skipNext bool + + out_after0 bool + out_header []byte + out_skip int + + in_after0 bool + in_header []byte + in_skip int } func NewXorConn(conn net.Conn, key []byte) *XorConn { @@ -26,29 +34,56 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records if len(b) == 0 { return 0, nil } - var iv []byte - if c.ctr == nil { - block, _ := aes.NewCipher(c.key) - iv = make([]byte, 16) - rand.Read(iv) - c.ctr = cipher.NewCTR(block, iv) - } - t, l, _ := DecodeHeader(b) - if t == 23 { // single 23 - l = 5 - } else { // 1/0 + 23, or noises only - l += 10 + if !c.out_after0 { + var iv []byte + if c.ctr == nil { + block, _ := aes.NewCipher(c.key) + iv = make([]byte, 16) + rand.Read(iv) + c.ctr = cipher.NewCTR(block, iv) + } + t, l, _ := DecodeHeader(b) + if t == 23 { // single 23 + l = 5 + } else { // 1/0 + 23, or noises only + l += 10 + if t == 0 { + c.out_after0 = true + } + } + c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b + if iv != nil { + b = append(iv, b...) + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + if iv != nil { + b = b[16:] // for len(b) + } + return len(b), nil } - c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b - if iv != nil { - b = append(iv, b...) + for p := b; ; { // for XTLS + if len(p) <= c.out_skip { + c.out_skip -= len(p) + break + } + p = p[c.out_skip:] + c.out_skip = 0 + need := 5 - len(c.out_header) + if len(p) < need { + c.out_header = append(c.out_header, p...) + c.ctr.XORKeyStream(p, p) + break + } + _, c.out_skip, _ = DecodeHeader(append(c.out_header, p[:need]...)) + c.out_header = make([]byte, 0, 5) // DO NOT CHANGE + c.ctr.XORKeyStream(p[:need], p[:need]) + p = p[need:] } if _, err := c.Conn.Write(b); err != nil { return 0, err } - if iv != nil { - b = b[16:] // for len(b) - } return len(b), nil } @@ -56,31 +91,56 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... if len(b) == 0 { return 0, nil } - if c.peerCtr == nil { - peerIv := make([]byte, 16) - if _, err := io.ReadFull(c.Conn, peerIv); err != nil { + if !c.in_after0 || !c.isHeader { + if c.peerCtr == nil { + peerIv := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, peerIv); err != nil { + return 0, err + } + block, _ := aes.NewCipher(c.key) + c.peerCtr = cipher.NewCTR(block, peerIv) + c.isHeader = true + } + if _, err := io.ReadFull(c.Conn, b); err != nil { return 0, err } - block, _ := aes.NewCipher(c.key) - c.peerCtr = cipher.NewCTR(block, peerIv) - c.isHeader = true - } - if _, err := io.ReadFull(c.Conn, b); err != nil { - return 0, err - } - if c.skipNext { - c.skipNext = false + if c.skipNext { + c.skipNext = false + return len(b), nil + } + c.peerCtr.XORKeyStream(b, b) + if c.isHeader { // always 5-bytes + if t, _, _ := DecodeHeader(b); t == 23 { + c.skipNext = true + } else { + c.isHeader = false + if t == 0 { + c.in_after0 = true + } + } + } else { + c.isHeader = true + } return len(b), nil } - c.peerCtr.XORKeyStream(b, b) - if c.isHeader { // always 5-bytes - if t, _, _ := DecodeHeader(b); t == 23 { - c.skipNext = true - } else { - c.isHeader = false + n, err := c.Conn.Read(b) + for p := b[:n]; ; { // for XTLS + if len(p) <= c.in_skip { + c.in_skip -= len(p) + break } - } else { - c.isHeader = true + p = p[c.in_skip:] + c.in_skip = 0 + need := 5 - len(c.in_header) + if len(p) < need { + c.peerCtr.XORKeyStream(p, p) + c.in_header = append(c.in_header, p...) + break + } + c.peerCtr.XORKeyStream(p[:need], p[:need]) + _, c.in_skip, _ = DecodeHeader(append(c.in_header, p[:need]...)) + c.in_header = make([]byte, 0, 5) // DO NOT CHANGE + p = p[need:] } - return len(b), nil + return n, err } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index deaf1cf18f0d..652dadd211dd 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -208,6 +208,14 @@ func (*Handler) Network() []net.Network { // Process implements proxy.Inbound.Process(). func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + 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() + } + } + iConn := connection if statConn, ok := iConn.(*stat.CounterConnection); ok { iConn = statConn.Connection @@ -218,14 +226,6 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s return errors.New("unable to set read deadline").Base(err).AtWarning() } - 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() - } - } - first := buf.FromBytes(make([]byte, buf.Size)) first.Clear() firstLen, errR := first.ReadFrom(connection) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 9be880390ba9..dd777314966e 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -103,13 +103,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } defer conn.Close() - iConn := conn - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } - 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) @@ -118,6 +111,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } } + iConn := conn + if statConn, ok := iConn.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + target := ob.Target + errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination().NetAddr()) + command := protocol.RequestCommandTCP if target.Network == net.Network_UDP { command = protocol.RequestCommandUDP From 84835bec7d0d8555d0dd30953ed26a272de814c4 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:20:27 +0000 Subject: [PATCH 18/25] Support VLESS Encryption (native/random) + XTLS Vision + Any Transport like XHTTP (UDS or not) + TLS/REALITY https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3200720109 --- proxy/proxy.go | 40 ++++++++++++++++++++------------ proxy/vless/encryption/client.go | 6 ++--- proxy/vless/encryption/server.go | 6 ++--- proxy/vless/encryption/xor.go | 6 +++-- proxy/vless/inbound/inbound.go | 19 +++++++++++---- proxy/vless/outbound/outbound.go | 22 ++++++++++++------ 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index 38f38b41ea7c..188cf4e93450 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -525,27 +525,37 @@ 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 clientConn, ok := conn.(*encryption.ClientConn); ok { + conn = clientConn.Conn + isEncryption = true + } + if serverConn, ok := conn.(*encryption.ServerConn); ok { + conn = serverConn.Conn + isEncryption = true + } + if xorConn, ok := conn.(*encryption.XorConn); ok { + return xorConn, nil, nil // xorConn should not be penetrated + } + if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection readCounter = statConn.ReadCounter writerCounter = statConn.WriteCounter } - if _, ok := conn.(*encryption.XorConn); ok { - return conn, readCounter, writerCounter - } - 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() @@ -636,7 +646,7 @@ 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) } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 5092de5f0ea1..101d2052c0b1 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -58,8 +58,8 @@ func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Dura if err != nil { return } - hash256 := sha3.Sum256(nfsEKeyBytes) - copy(i.hash11[:], hash256[:]) + hash32 := sha3.Sum256(nfsEKeyBytes) + copy(i.hash11[:], hash32[:]) if xor > 0 { xorKey := sha3.Sum256(nfsEKeyBytes) i.xorKey = xorKey[:] @@ -68,7 +68,7 @@ func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Dura return } -func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { +func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { if i.nfsEKey == nil { return nil, errors.New("uninitialized") } diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index c1d996110f34..a9ade3d407a6 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -55,8 +55,8 @@ func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Durat if err != nil { return } - hash256 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - copy(i.hash11[:], hash256[:]) + hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + copy(i.hash11[:], hash32[:]) if xor > 0 { xorKey := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) i.xorKey = xorKey[:] @@ -92,7 +92,7 @@ func (i *ServerInstance) Close() (err error) { return } -func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { +func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { if i.nfsDKey == nil { return nil, errors.New("uninitialized") } diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index 69ff15780ffb..caad12bf076e 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -49,6 +49,7 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records l += 10 if t == 0 { c.out_after0 = true + c.out_header = make([]byte, 0, 5) // important } } c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b @@ -77,7 +78,7 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records break } _, c.out_skip, _ = DecodeHeader(append(c.out_header, p[:need]...)) - c.out_header = make([]byte, 0, 5) // DO NOT CHANGE + c.out_header = c.out_header[:0] c.ctr.XORKeyStream(p[:need], p[:need]) p = p[need:] } @@ -116,6 +117,7 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... c.isHeader = false if t == 0 { c.in_after0 = true + c.in_header = make([]byte, 0, 5) // important } } } else { @@ -139,7 +141,7 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... } c.peerCtr.XORKeyStream(p[:need], p[:need]) _, c.in_skip, _ = DecodeHeader(append(c.in_header, p[:need]...)) - c.in_header = make([]byte, 0, 5) // DO NOT CHANGE + c.in_header = c.in_header[:0] p = p[need:] } return n, err diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 652dadd211dd..39a192b84725 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -12,6 +12,7 @@ import ( "time" "unsafe" + "github.com/pires/go-proxyproto" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -31,6 +32,7 @@ import ( "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" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -208,6 +210,11 @@ func (*Handler) Network() []net.Network { // Process implements proxy.Inbound.Process(). func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + iConn := connection + if statConn, ok := iConn.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + if h.decryption != nil { var err error connection, err = h.decryption.Handshake(connection) @@ -216,11 +223,6 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s } } - iConn := connection - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } - 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() @@ -499,6 +501,13 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s case protocol.RequestCommandTCP: if serverConn, ok := connection.(*encryption.ServerConn); ok { peerCache = &serverConn.PeerCache + _, ok0 := serverConn.Conn.(*encryption.XorConn) + _, ok1 := iConn.(*proxyproto.Conn) + _, ok2 := iConn.(*net.TCPConn) + _, ok3 := iConn.(*internet.UnixConnWrapper) + if ok0 || (!ok1 && !ok2 && !ok3) { + inbound.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + } break } var t reflect.Type diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index dd777314966e..42b2a6083109 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -9,6 +9,7 @@ import ( "time" "unsafe" + "github.com/pires/go-proxyproto" utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -103,6 +104,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } defer conn.Close() + iConn := conn + if statConn, ok := iConn.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + 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) @@ -111,13 +119,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } } - iConn := conn - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } - target := ob.Target - errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination().NetAddr()) - command := protocol.RequestCommandTCP if target.Network == net.Network_UDP { command = protocol.RequestCommandUDP @@ -161,6 +162,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte case protocol.RequestCommandTCP: if clientConn, ok := conn.(*encryption.ClientConn); ok { peerCache = &clientConn.PeerCache + _, ok0 := clientConn.Conn.(*encryption.XorConn) + _, ok1 := iConn.(*proxyproto.Conn) + _, ok2 := iConn.(*net.TCPConn) + _, ok3 := iConn.(*internet.UnixConnWrapper) + if ok0 || (!ok1 && !ok2 && !ok3) { + ob.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + } break } var t reflect.Type From 373558ed7abdbac3de41745cf30ec04c9adde604 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:17:35 +0000 Subject: [PATCH 19/25] Use X25519 for XOR; Add "divide" (ECH, before and includes type 0); Change config format https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3207449672 --- infra/conf/vless.go | 40 ++++++++------ main/commands/all/curve25519.go | 41 +++++--------- main/commands/all/mlkem768.go | 9 ++- main/commands/all/x25519.go | 4 +- proxy/proxy.go | 16 +++++- proxy/vless/account.go | 6 +- proxy/vless/account.pb.go | 36 ++++++------ proxy/vless/account.proto | 2 +- proxy/vless/encryption/client.go | 33 ++++++----- proxy/vless/encryption/common.go | 2 +- proxy/vless/encryption/server.go | 36 +++++++----- proxy/vless/encryption/xor.go | 94 +++++++++++++++++++++++++------- proxy/vless/inbound/config.pb.go | 29 +++++----- proxy/vless/inbound/config.proto | 2 +- proxy/vless/inbound/inbound.go | 17 ++---- proxy/vless/outbound/outbound.go | 17 +++--- 16 files changed, 225 insertions(+), 159 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 5fcdf34c3801..44503912824e 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -71,8 +71,8 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Decryption = c.Decryption if !func() bool { - s := strings.SplitN(config.Decryption, "-", 4) - if len(s) != 4 || s[2] != "mlkem768seed" { + s := strings.Split(config.Decryption, ".") + if len(s) != 5 || s[2] != "mlkem768Seed" { return false } if s[0] != "1rtt" { @@ -87,17 +87,21 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Minutes = uint32(i) } switch s[1] { - case "vless": - case "xored": - config.Xor = 1 + case "native": + case "divide": + config.XorMode = 1 + case "random": + config.XorMode = 2 default: return false } - b, err := base64.RawURLEncoding.DecodeString(s[3]) - if len(b) != 64 || err != nil { + if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { return false } - config.Decryption = s[3] + if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 64 { + return false + } + config.Decryption = s[4] + "." + s[3] return true }() && config.Decryption != "none" { if config.Decryption == "" { @@ -215,8 +219,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } if !func() bool { - s := strings.SplitN(account.Encryption, "-", 4) - if len(s) != 4 || s[2] != "mlkem768client" { + s := strings.Split(account.Encryption, ".") + if len(s) != 5 || s[2] != "mlkem768Client" { return false } if s[0] != "1rtt" { @@ -231,17 +235,21 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { account.Minutes = uint32(i) } switch s[1] { - case "vless": - case "xored": - account.Xor = 1 + case "native": + case "divide": + account.XorMode = 1 + case "random": + account.XorMode = 2 default: return false } - b, err := base64.RawURLEncoding.DecodeString(s[3]) - if len(b) != 1184 || err != nil { + if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { + return false + } + if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 1184 { return false } - account.Encryption = s[3] + account.Encryption = s[4] + "." + s[3] return true }() && account.Encryption != "none" { if account.Encryption == "" { diff --git a/main/commands/all/curve25519.go b/main/commands/all/curve25519.go index bb706c6c2ac5..ddaebdfd2e72 100644 --- a/main/commands/all/curve25519.go +++ b/main/commands/all/curve25519.go @@ -1,17 +1,13 @@ package all import ( + "crypto/ecdh" "crypto/rand" "encoding/base64" "fmt" - - "golang.org/x/crypto/curve25519" ) 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,24 +15,17 @@ 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: @@ -45,14 +34,12 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { 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", + fmt.Printf("PrivateKey: %v\nPassword: %v", encoding.EncodeToString(privateKey), - encoding.EncodeToString(publicKey)) -out: - fmt.Println(output) + encoding.EncodeToString(key.PublicKey().Bytes())) } diff --git a/main/commands/all/mlkem768.go b/main/commands/all/mlkem768.go index 78512bad0a59..f3cb0f79a095 100644 --- a/main/commands/all/mlkem768.go +++ b/main/commands/all/mlkem768.go @@ -3,6 +3,7 @@ package all import ( "crypto/mlkem" "crypto/rand" + "crypto/sha3" "encoding/base64" "fmt" @@ -40,8 +41,10 @@ func executeMLKEM768(cmd *base.Command, args []string) { rand.Read(seed[:]) } key, _ := mlkem.NewDecapsulationKey768(seed[:]) - pub := key.EncapsulationKey() - fmt.Printf("Seed: %v\nClient: %v", + client := key.EncapsulationKey().Bytes() + hash32 := sha3.Sum256(client) + fmt.Printf("Seed: %v\nClient: %v\nHash11: %v", base64.RawURLEncoding.EncodeToString(seed[:]), - base64.RawURLEncoding.EncodeToString(pub.Bytes())) + base64.RawURLEncoding.EncodeToString(client), + base64.RawURLEncoding.EncodeToString(hash32[:11])) } diff --git a/main/commands/all/x25519.go b/main/commands/all/x25519.go index 607562b61c81..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 (REALITY)`, + Short: `Generate key pair for X25519 key exchange (VLESS, REALITY)`, Long: ` -Generate key pair for X25519 key exchange (REALITY). +Generate key pair for X25519 key exchange (VLESS, REALITY). Random: {{.Exec}} x25519 diff --git a/proxy/proxy.go b/proxy/proxy.go index 188cf4e93450..8251849dba08 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -539,7 +539,10 @@ func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { isEncryption = true } if xorConn, ok := conn.(*encryption.XorConn); ok { - return xorConn, nil, nil // xorConn should not be penetrated + if !xorConn.Divide { + return xorConn, nil, nil // full-random xorConn should not be penetrated + } + conn = xorConn.Conn } if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection @@ -652,3 +655,14 @@ func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer sign } 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 55c7b54c8db6..9967c7e1169f 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,7 +18,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? - Xor: a.Xor, + XorMode: a.XorMode, Minutes: a.Minutes, }, nil } @@ -31,7 +31,7 @@ type MemoryAccount struct { Flow string Encryption string - Xor uint32 + XorMode uint32 Minutes uint32 } @@ -49,7 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, - Xor: a.Xor, + XorMode: a.XorMode, Minutes: a.Minutes, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index 6cc66f04cc9f..ca638de11667 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -30,7 +30,7 @@ type Account struct { // Flow settings. May be "xtls-rprx-vision". Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` - Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } @@ -85,9 +85,9 @@ func (x *Account) GetEncryption() string { return "" } -func (x *Account) GetXor() uint32 { +func (x *Account) GetXorMode() uint32 { if x != nil { - return x.Xor + return x.XorMode } return 0 } @@ -104,21 +104,21 @@ 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, 0x79, 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, 0x10, 0x0a, 0x03, - 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, 0x6f, 0x72, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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, + 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, 0x6d, 0x69, 0x6e, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, + 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 199005adee6a..f91a25281ec0 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -13,6 +13,6 @@ message Account { string flow = 2; string encryption = 3; - uint32 xor = 4; + uint32 xorMode = 4; uint32 minutes = 5; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 101d2052c0b1..7eff2eb8fd62 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -3,6 +3,7 @@ package encryption import ( "bytes" "crypto/cipher" + "crypto/ecdh" "crypto/mlkem" "crypto/rand" "crypto/sha3" @@ -29,7 +30,8 @@ type ClientInstance struct { sync.RWMutex nfsEKey *mlkem.EncapsulationKey768 hash11 [11]byte // no more capacity - xorKey []byte + xorMode uint32 + xorPKey *ecdh.PublicKey minutes time.Duration expire time.Time baseKey []byte @@ -49,22 +51,23 @@ type ClientConn struct { PeerCache []byte } -func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { +func (i *ClientInstance) Init(nfsEKeyBytes, xorPKeyBytes []byte, xorMode, minutes uint32) (err error) { if i.nfsEKey != nil { err = errors.New("already initialized") return } - i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes) - if err != nil { + if i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes); err != nil { return } hash32 := sha3.Sum256(nfsEKeyBytes) copy(i.hash11[:], hash32[:]) - if xor > 0 { - xorKey := sha3.Sum256(nfsEKeyBytes) - i.xorKey = xorKey[:] + if xorMode > 0 { + i.xorMode = xorMode + if i.xorPKey, err = ecdh.X25519().NewPublicKey(xorPKeyBytes); err != nil { + return + } } - i.minutes = minutes + i.minutes = time.Duration(minutes) * time.Minute return } @@ -72,8 +75,8 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { if i.nfsEKey == nil { return nil, errors.New("uninitialized") } - if i.xorKey != nil { - conn = NewXorConn(conn, i.xorKey) + if i.xorMode > 0 { + conn, _ = NewXorConn(conn, i.xorMode, i.xorPKey, nil) } c := &ClientConn{Conn: conn} @@ -134,7 +137,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { } c.baseKey = append(pfsKey, nfsKey...) - VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) + VLESS, _ := NewAEAD(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) if !bytes.Equal(VLESS, []byte("VLESS")) { return nil, errors.New("invalid server").AtError() } @@ -169,7 +172,7 @@ func (c *ClientConn) Write(b []byte) (int, error) { rand.Read(c.random) copy(data[5+32:], c.random) EncodeHeader(data[5+32+32:], 23, len(b)+16) - c.aead = NewAead(ClientCipher, c.baseKey, c.random, c.ticket) + c.aead = NewAEAD(ClientCipher, c.baseKey, c.random, c.ticket) c.nonce = make([]byte, 12) c.aead.Seal(data[:5+32+32+5], c.nonce, b, data[5+32+32:5+32+32+5]) } else { @@ -177,7 +180,7 @@ func (c *ClientConn) Write(b []byte) (int, error) { EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) + c.aead = NewAEAD(ClientCipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) @@ -218,7 +221,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if c.random == nil { return 0, errors.New("empty c.random") } - c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandomHello, c.random) + c.peerAead = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -243,7 +246,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(ClientCipher, c.baseKey, peerData, h) + peerAead = NewAEAD(ClientCipher, c.baseKey, peerData, h) } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index d4484bd6787b..6de517e0a874 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -72,7 +72,7 @@ func ReadAndDiscardPaddings(conn net.Conn) (h []byte, t byte, l int, err error) } } -func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { +func NewAEAD(c byte, secret, salt, info []byte) (aead cipher.AEAD) { key, _ := hkdf.Key(sha3.New256, secret, salt, string(info), 32) if c&1 == 1 { block, _ := aes.NewCipher(key) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index a9ade3d407a6..17c8c689d0a9 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -3,6 +3,7 @@ package encryption import ( "bytes" "crypto/cipher" + "crypto/ecdh" "crypto/mlkem" "crypto/rand" "crypto/sha3" @@ -27,7 +28,8 @@ type ServerInstance struct { sync.RWMutex nfsDKey *mlkem.DecapsulationKey768 hash11 [11]byte // no more capacity - xorKey []byte + xorMode uint32 + xorSKey *ecdh.PrivateKey minutes time.Duration sessions map[[32]byte]*ServerSession closed bool @@ -46,23 +48,24 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Duration) (err error) { +func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes uint32) (err error) { if i.nfsDKey != nil { err = errors.New("already initialized") return } - i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed) - if err != nil { + if i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed); err != nil { return } hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) copy(i.hash11[:], hash32[:]) - if xor > 0 { - xorKey := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - i.xorKey = xorKey[:] + if xorMode > 0 { + i.xorMode = xorMode + if i.xorSKey, err = ecdh.X25519().NewPrivateKey(xorSKeyBytes); err != nil { + return + } } if minutes > 0 { - i.minutes = minutes + i.minutes = time.Duration(minutes) * time.Minute i.sessions = make(map[[32]byte]*ServerSession) go func() { for { @@ -96,8 +99,11 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { if i.nfsDKey == nil { return nil, errors.New("uninitialized") } - if i.xorKey != nil { - conn = NewXorConn(conn, i.xorKey) + if i.xorMode > 0 { + var err error + if conn, err = NewXorConn(conn, i.xorMode, nil, i.xorSKey); err != nil { + return nil, err + } } c := &ServerConn{Conn: conn} @@ -168,7 +174,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) - c.ticket = append(i.hash11[:], NewAead(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) + c.ticket = append(i.hash11[:], NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) paddingLen := crypto.RandBetween(100, 1000) @@ -222,7 +228,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } c.peerRandom = peerTicketHello[32:] } - c.peerAead = NewAead(c.cipher, c.baseKey, c.peerRandom, c.ticket) + c.peerAead = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -247,7 +253,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(c.cipher, c.baseKey, peerData, h) + peerAead = NewAEAD(c.cipher, c.baseKey, peerData, h) } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { @@ -283,7 +289,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { EncodeHeader(data, 0, 32) rand.Read(data[5 : 5+32]) EncodeHeader(data[5+32:], 23, len(b)+16) - c.aead = NewAead(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) + c.aead = NewAEAD(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) c.nonce = make([]byte, 12) c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5]) } else { @@ -291,7 +297,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAead(c.cipher, c.baseKey, data[5:], data[:5]) + c.aead = NewAEAD(c.cipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index caad12bf076e..bac45e46de70 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -3,13 +3,21 @@ package encryption import ( "crypto/aes" "crypto/cipher" + "crypto/ecdh" + "crypto/hkdf" "crypto/rand" + "crypto/sha3" "io" "net" + + "github.com/xtls/xray-core/common/errors" ) type XorConn struct { net.Conn + Divide bool + + head []byte key []byte ctr cipher.Stream peerCtr cipher.Stream @@ -25,8 +33,55 @@ type XorConn struct { in_skip int } -func NewXorConn(conn net.Conn, key []byte) *XorConn { - return &XorConn{Conn: conn, key: key} +func NewCTR(key, iv []byte, isServer bool) cipher.Stream { + info := "CLIENT" + if isServer { + info = "SERVER" // avoids attackers sending traffic back to the client, though the encryption layer has its own protection + } + key, _ = hkdf.Key(sha3.New256, key, iv, info, 32) // avoids using pKey directly if attackers sent the basepoint, or whaterver they like + block, _ := aes.NewCipher(key) + return cipher.NewCTR(block, iv) +} + +func NewXorConn(conn net.Conn, mode uint32, pKey *ecdh.PublicKey, sKey *ecdh.PrivateKey) (*XorConn, error) { + if mode == 0 || (pKey == nil && sKey == nil) || (pKey != nil && sKey != nil) { + return nil, errors.New("invalid parameters") + } + c := &XorConn{ + Conn: conn, + Divide: mode == 1, + isHeader: true, + out_header: make([]byte, 0, 5), // important + in_header: make([]byte, 0, 5), // important + } + if pKey != nil { + c.head = make([]byte, 16+32) + rand.Read(c.head) + eSKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + NewCTR(pKey.Bytes(), c.head[:16], false).XORKeyStream(c.head[16:], eSKey.PublicKey().Bytes()) // make X25519 public key distinguishable from random bytes + c.key, _ = eSKey.ECDH(pKey) + c.ctr = NewCTR(c.key, c.head[:16], false) + } + if sKey != nil { + peerHead := make([]byte, 16+32) + if _, err := io.ReadFull(c.Conn, peerHead); err != nil { + return nil, err + } + NewCTR(sKey.PublicKey().Bytes(), peerHead[:16], false).XORKeyStream(peerHead[16:], peerHead[16:]) // we don't use buggy elligator, because we have PSK :) + ePKey, err := ecdh.X25519().NewPublicKey(peerHead[16:]) + if err != nil { + return nil, err + } + key, err := sKey.ECDH(ePKey) + if err != nil { + return nil, err + } + c.peerCtr = NewCTR(key, peerHead[:16], false) + c.head = make([]byte, 16) + rand.Read(c.head) // make sure the server always replies random bytes even when received replays, though it is not important + c.ctr = NewCTR(key, c.head, true) // the same key links the upload & download, though the encryption layer has its own link + } + return c, nil //chacha20.NewUnauthenticatedCipher() } @@ -35,13 +90,6 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records return 0, nil } if !c.out_after0 { - var iv []byte - if c.ctr == nil { - block, _ := aes.NewCipher(c.key) - iv = make([]byte, 16) - rand.Read(iv) - c.ctr = cipher.NewCTR(block, iv) - } t, l, _ := DecodeHeader(b) if t == 23 { // single 23 l = 5 @@ -49,20 +97,24 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records l += 10 if t == 0 { c.out_after0 = true - c.out_header = make([]byte, 0, 5) // important + if c.Divide { + l -= 5 + } } } c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b - if iv != nil { - b = append(iv, b...) + l = len(b) + if c.head != nil { + b = append(c.head, b...) + c.head = nil } if _, err := c.Conn.Write(b); err != nil { return 0, err } - if iv != nil { - b = b[16:] // for len(b) - } - return len(b), nil + return l, nil + } + if c.Divide { + return c.Conn.Write(b) } for p := b; ; { // for XTLS if len(p) <= c.out_skip { @@ -93,14 +145,12 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... return 0, nil } if !c.in_after0 || !c.isHeader { - if c.peerCtr == nil { + if c.peerCtr == nil { // for client peerIv := make([]byte, 16) if _, err := io.ReadFull(c.Conn, peerIv); err != nil { return 0, err } - block, _ := aes.NewCipher(c.key) - c.peerCtr = cipher.NewCTR(block, peerIv) - c.isHeader = true + c.peerCtr = NewCTR(c.key, peerIv, true) } if _, err := io.ReadFull(c.Conn, b); err != nil { return 0, err @@ -117,7 +167,6 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... c.isHeader = false if t == 0 { c.in_after0 = true - c.in_header = make([]byte, 0, 5) // important } } } else { @@ -125,6 +174,9 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... } return len(b), nil } + if c.Divide { + return c.Conn.Read(b) + } n, err := c.Conn.Read(b) for p := b[:n]; ; { // for XTLS if len(p) <= c.in_skip { diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index a125a7e42646..240c25d99c52 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -114,7 +114,7 @@ type Config struct { 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"` - Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } @@ -169,9 +169,9 @@ func (x *Config) GetDecryption() string { return "" } -func (x *Config) GetXor() uint32 { +func (x *Config) GetXorMode() uint32 { if x != nil { - return x.Xor + return x.XorMode } return 0 } @@ -199,7 +199,7 @@ 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, 0xcc, 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, @@ -210,16 +210,17 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 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, 0x10, 0x0a, 0x03, 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, - 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 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, 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, 0x6d, 0x69, + 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, + 0x75, 0x74, 0x65, 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 c96855a0619c..186d8588feb3 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -22,6 +22,6 @@ message Config { repeated Fallback fallbacks = 2; string decryption = 3; - uint32 xor = 4; + uint32 xorMode = 4; uint32 minutes = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 39a192b84725..fc8dd243e6e8 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -12,7 +12,6 @@ import ( "time" "unsafe" - "github.com/pires/go-proxyproto" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -32,7 +31,6 @@ import ( "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" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -86,10 +84,11 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } - d, _ := base64.RawURLEncoding.DecodeString(config.Decryption) - if len(d) == 64 { + if s := strings.Split(config.Decryption, "."); len(s) == 2 { + nfsDKeySeed, _ := base64.RawURLEncoding.DecodeString(s[0]) + xorSKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) handler.decryption = &encryption.ServerInstance{} - if err := handler.decryption.Init(d, config.Xor, time.Duration(config.Minutes)*time.Minute); err != nil { + if err := handler.decryption.Init(nfsDKeySeed, xorSKeyBytes, config.XorMode, config.Minutes); err != nil { return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() } } @@ -501,12 +500,8 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s case protocol.RequestCommandTCP: if serverConn, ok := connection.(*encryption.ServerConn); ok { peerCache = &serverConn.PeerCache - _, ok0 := serverConn.Conn.(*encryption.XorConn) - _, ok1 := iConn.(*proxyproto.Conn) - _, ok2 := iConn.(*net.TCPConn) - _, ok3 := iConn.(*internet.UnixConnWrapper) - if ok0 || (!ok1 && !ok2 && !ok3) { - inbound.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + if xorConn, ok := serverConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice } break } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 42b2a6083109..519748254f30 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -6,10 +6,10 @@ import ( gotls "crypto/tls" "encoding/base64" "reflect" + "strings" "time" "unsafe" - "github.com/pires/go-proxyproto" utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -69,10 +69,11 @@ func New(ctx context.Context, config *Config) (*Handler, error) { } a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) - e, _ := base64.RawURLEncoding.DecodeString(a.Encryption) - if len(e) == 1184 { + if s := strings.Split(a.Encryption, "."); len(s) == 2 { + nfsEKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[0]) + xorPKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) handler.encryption = &encryption.ClientInstance{} - if err := handler.encryption.Init(e, a.Xor, time.Duration(a.Minutes)*time.Minute); err != nil { + if err := handler.encryption.Init(nfsEKeyBytes, xorPKeyBytes, a.XorMode, a.Minutes); err != nil { return nil, errors.New("failed to use mlkem768client").Base(err).AtError() } } @@ -162,12 +163,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte case protocol.RequestCommandTCP: if clientConn, ok := conn.(*encryption.ClientConn); ok { peerCache = &clientConn.PeerCache - _, ok0 := clientConn.Conn.(*encryption.XorConn) - _, ok1 := iConn.(*proxyproto.Conn) - _, ok2 := iConn.(*net.TCPConn) - _, ok3 := iConn.(*internet.UnixConnWrapper) - if ok0 || (!ok1 && !ok2 && !ok3) { - ob.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + if xorConn, ok := clientConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice } break } From 38cc306c955c362f044e074049a5e67b6b9fb389 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:27:09 +0000 Subject: [PATCH 20/25] Use nfsAEAD & pfsAEAD for paddings; Refine comments https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3210125349 https://github.com/XTLS/Xray-core/commit/373558ed7abdbac3de41745cf30ec04c9adde604#r164355865 --- main/commands/all/curve25519.go | 3 ++- proxy/vless/encryption/client.go | 28 ++++++++++++++------------- proxy/vless/encryption/server.go | 33 ++++++++++++++++---------------- proxy/vless/encryption/xor.go | 2 +- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/main/commands/all/curve25519.go b/main/commands/all/curve25519.go index ddaebdfd2e72..c3c516ad526a 100644 --- a/main/commands/all/curve25519.go +++ b/main/commands/all/curve25519.go @@ -29,7 +29,8 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { } // 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 diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 7eff2eb8fd62..4f86b6a2a42e 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -46,7 +46,7 @@ type ClientConn struct { random []byte aead cipher.AEAD nonce []byte - peerAead cipher.AEAD + peerAEAD cipher.AEAD peerNonce []byte PeerCache []byte } @@ -97,21 +97,23 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { pfsDKey, _ := mlkem.NewDecapsulationKey768(pfsDKeySeed) pfsEKeyBytes := pfsDKey.EncapsulationKey().Bytes() nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate() - paddingLen := crypto.RandBetween(100, 1000) + nfsAEAD := NewAEAD(ClientCipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey) - clientHello := make([]byte, 5+11+1+1184+1088+5+paddingLen) + clientHello := make([]byte, 5+11+1+1184+1088+crypto.RandBetween(100, 1000)) EncodeHeader(clientHello, 1, 11+1+1184+1088) copy(clientHello[5:], i.hash11[:]) clientHello[5+11] = ClientCipher copy(clientHello[5+11+1:], pfsEKeyBytes) copy(clientHello[5+11+1+1184:], encapsulatedNfsKey) - EncodeHeader(clientHello[5+11+1+1184+1088:], 23, int(paddingLen)) - rand.Read(clientHello[5+11+1+1184+1088+5:]) + padding := clientHello[5+11+1+1184+1088:] + rand.Read(padding) // important + EncodeHeader(padding, 23, len(padding)-5) + nfsAEAD.Seal(padding[:5], clientHello[5:5+11+1], padding[5:len(padding)-16], padding[:5]) if _, err := c.Conn.Write(clientHello); err != nil { return nil, err } - // client can send more paddings / NFS AEAD messages if needed + // client can send more NFS AEAD paddings / messages if needed _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before server hello if err != nil { @@ -195,7 +197,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - if c.peerAead == nil { + if c.peerAEAD == nil { _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before random hello if err != nil { if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT's 0-RTT @@ -221,7 +223,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if c.random == nil { return 0, errors.New("empty c.random") } - c.peerAead = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random) + c.peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -244,13 +246,13 @@ func (c *ClientConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } - var peerAead cipher.AEAD + var peerAEAD cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAEAD(ClientCipher, c.baseKey, peerData, h) + peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerData, h) } - _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) - if peerAead != nil { - c.peerAead = peerAead + _, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h) + if peerAEAD != nil { + c.peerAEAD = peerAEAD } IncreaseNonce(c.peerNonce) if err != nil { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 17c8c689d0a9..8d91f415a722 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -41,7 +41,7 @@ type ServerConn struct { baseKey []byte ticket []byte peerRandom []byte - peerAead cipher.AEAD + peerAEAD cipher.AEAD peerNonce []byte PeerCache []byte aead cipher.AEAD @@ -173,22 +173,23 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { } pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) + pfsAEAD := NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey) + c.ticket = append(i.hash11[:], pfsAEAD.Seal(nil, peerClientHello[:11+1], []byte("VLESS"), pfsEKeyBytes)...) + IncreaseNonce(peerClientHello[:11+1]) - c.ticket = append(i.hash11[:], NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) - - paddingLen := crypto.RandBetween(100, 1000) - - serverHello := make([]byte, 5+1088+21+5+paddingLen) + serverHello := make([]byte, 5+1088+21+crypto.RandBetween(100, 1000)) EncodeHeader(serverHello, 1, 1088+21) copy(serverHello[5:], encapsulatedPfsKey) copy(serverHello[5+1088:], c.ticket[11:]) - EncodeHeader(serverHello[5+1088+21:], 23, int(paddingLen)) - rand.Read(serverHello[5+1088+21+5:]) + padding := serverHello[5+1088+21:] + rand.Read(padding) // important + EncodeHeader(padding, 23, len(padding)-5) + pfsAEAD.Seal(padding[:5], peerClientHello[:11+1], padding[5:len(padding)-16], padding[:5]) if _, err := c.Conn.Write(serverHello); err != nil { return nil, err } - // server can send more paddings / PFS AEAD messages if needed + // server can send more PFS AEAD paddings / messages if needed if i.minutes > 0 { i.Lock() @@ -207,7 +208,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - if c.peerAead == nil { + if c.peerAEAD == nil { if c.peerRandom == nil { // 1-RTT's 0-RTT _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before ticket hello if err != nil { @@ -228,7 +229,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } c.peerRandom = peerTicketHello[32:] } - c.peerAead = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) + c.peerAEAD = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -251,13 +252,13 @@ func (c *ServerConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } - var peerAead cipher.AEAD + var peerAEAD cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAEAD(c.cipher, c.baseKey, peerData, h) + peerAEAD = NewAEAD(c.cipher, c.baseKey, peerData, h) } - _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) - if peerAead != nil { - c.peerAead = peerAead + _, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h) + if peerAEAD != nil { + c.peerAEAD = peerAEAD } IncreaseNonce(c.peerNonce) if err != nil { diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index bac45e46de70..c5586ae915ed 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -57,7 +57,7 @@ func NewXorConn(conn net.Conn, mode uint32, pKey *ecdh.PublicKey, sKey *ecdh.Pri if pKey != nil { c.head = make([]byte, 16+32) rand.Read(c.head) - eSKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + eSKey, _ := ecdh.X25519().NewPrivateKey(c.head[16:]) NewCTR(pKey.Bytes(), c.head[:16], false).XORKeyStream(c.head[16:], eSKey.PublicKey().Bytes()) // make X25519 public key distinguishable from random bytes c.key, _ = eSKey.ECDH(pKey) c.ctr = NewCTR(c.key, c.head[:16], false) From b33555cc0a52d0af3c23d2af8fca42f8a685d9af Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:41:56 +0000 Subject: [PATCH 21/25] Empty hash11 for "native"; Verify NFS paddings; Fix 1-RTT's `peerRandom` https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3213837443 --- proxy/vless/encryption/client.go | 10 +++--- proxy/vless/encryption/common.go | 11 ++++-- proxy/vless/encryption/server.go | 61 ++++++++++++++++---------------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 4f86b6a2a42e..a0ae0d2b763f 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -59,13 +59,13 @@ func (i *ClientInstance) Init(nfsEKeyBytes, xorPKeyBytes []byte, xorMode, minute if i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes); err != nil { return } - hash32 := sha3.Sum256(nfsEKeyBytes) - copy(i.hash11[:], hash32[:]) if xorMode > 0 { i.xorMode = xorMode if i.xorPKey, err = ecdh.X25519().NewPublicKey(xorPKeyBytes); err != nil { return } + hash32 := sha3.Sum256(nfsEKeyBytes) + copy(i.hash11[:], hash32[:]) } i.minutes = time.Duration(minutes) * time.Minute return @@ -115,7 +115,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { } // client can send more NFS AEAD paddings / messages if needed - _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before server hello + _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before server hello if err != nil { return nil, err } @@ -198,9 +198,9 @@ func (c *ClientConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAEAD == nil { - _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before random hello + _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before random hello if err != nil { - if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT's 0-RTT + if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT c.instance.Lock() if bytes.Equal(c.ticket, c.instance.ticket) { c.instance.expire = time.Now() // expired diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 6de517e0a874..4e2d475600ab 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -61,14 +61,21 @@ func ReadAndDecodeHeader(conn net.Conn) (h []byte, t byte, l int, err error) { return } -func ReadAndDiscardPaddings(conn net.Conn) (h []byte, t byte, l int, err error) { +func ReadAndDiscardPaddings(conn net.Conn, aead cipher.AEAD, nonce []byte) (h []byte, t byte, l int, err error) { for { if h, t, l, err = ReadAndDecodeHeader(conn); err != nil || t != 23 { return } - if _, err = io.ReadFull(conn, make([]byte, l)); err != nil { + padding := make([]byte, l) + if _, err = io.ReadFull(conn, padding); err != nil { return } + if aead != nil { + if _, err := aead.Open(nil, nonce, padding, h); err != nil { + return h, t, l, err + } + IncreaseNonce(nonce) + } } } diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 8d91f415a722..336571b4ea49 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -56,13 +56,13 @@ func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes if i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed); err != nil { return } - hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - copy(i.hash11[:], hash32[:]) if xorMode > 0 { i.xorMode = xorMode if i.xorSKey, err = ecdh.X25519().NewPrivateKey(xorSKeyBytes); err != nil { return } + hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) + copy(i.hash11[:], hash32[:]) } if minutes > 0 { i.minutes = time.Duration(minutes) * time.Minute @@ -107,7 +107,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { } c := &ServerConn{Conn: conn} - _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before client/ticket hello + _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before client/ticket hello if err != nil { return nil, err } @@ -171,11 +171,14 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { if err != nil { return nil, err } + nfsAEAD := NewAEAD(c.cipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey) + nfsNonce := append([]byte{}, peerClientHello[:11+1]...) pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) pfsAEAD := NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey) - c.ticket = append(i.hash11[:], pfsAEAD.Seal(nil, peerClientHello[:11+1], []byte("VLESS"), pfsEKeyBytes)...) - IncreaseNonce(peerClientHello[:11+1]) + pfsNonce := append([]byte{}, peerClientHello[:11+1]...) + c.ticket = append(i.hash11[:], pfsAEAD.Seal(nil, pfsNonce, []byte("VLESS"), pfsEKeyBytes)...) + IncreaseNonce(pfsNonce) serverHello := make([]byte, 5+1088+21+crypto.RandBetween(100, 1000)) EncodeHeader(serverHello, 1, 1088+21) @@ -184,20 +187,41 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { padding := serverHello[5+1088+21:] rand.Read(padding) // important EncodeHeader(padding, 23, len(padding)-5) - pfsAEAD.Seal(padding[:5], peerClientHello[:11+1], padding[5:len(padding)-16], padding[:5]) + pfsAEAD.Seal(padding[:5], pfsNonce, padding[5:len(padding)-16], padding[:5]) if _, err := c.Conn.Write(serverHello); err != nil { return nil, err } // server can send more PFS AEAD paddings / messages if needed + _, t, l, err = ReadAndDiscardPaddings(c.Conn, nfsAEAD, nfsNonce) // allow paddings before ticket hello + if err != nil { + return nil, err + } + if t != 0 { + return nil, errors.New("unexpected type ", t, ", expect ticket hello") + } + peerTicketHello := make([]byte, 32+32) + if l != len(peerTicketHello) { + return nil, errors.New("unexpected length ", l, " for ticket hello") + } + if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { + return nil, err + } + if !bytes.Equal(peerTicketHello[:32], c.ticket) { + return nil, errors.New("naughty boy") + } + c.peerRandom = peerTicketHello[32:] + if i.minutes > 0 { i.Lock() - i.sessions[[32]byte(c.ticket)] = &ServerSession{ + s := &ServerSession{ expire: time.Now().Add(i.minutes), cipher: c.cipher, baseKey: c.baseKey, } + s.randoms.Store([32]byte(c.peerRandom), true) + i.sessions[[32]byte(c.ticket)] = s i.Unlock() } @@ -209,26 +233,6 @@ func (c *ServerConn) Read(b []byte) (int, error) { return 0, nil } if c.peerAEAD == nil { - if c.peerRandom == nil { // 1-RTT's 0-RTT - _, t, l, err := ReadAndDiscardPaddings(c.Conn) // allow paddings before ticket hello - if err != nil { - return 0, err - } - if t != 0 { - return 0, errors.New("unexpected type ", t, ", expect ticket hello") - } - peerTicketHello := make([]byte, 32+32) - if l != len(peerTicketHello) { - return 0, errors.New("unexpected length ", l, " for ticket hello") - } - if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { - return 0, err - } - if !bytes.Equal(peerTicketHello[:32], c.ticket) { - return 0, errors.New("naughty boy") - } - c.peerRandom = peerTicketHello[32:] - } c.peerAEAD = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } @@ -283,9 +287,6 @@ func (c *ServerConn) Write(b []byte) (int, error) { } n += len(b) if c.aead == nil { - if c.peerRandom == nil { - return 0, errors.New("empty c.peerRandom") - } data = make([]byte, 5+32+5+len(b)+16) EncodeHeader(data, 0, 32) rand.Read(data[5 : 5+32]) From ad7140641c44239c9dcdc3d7215ea639b1f0841c Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 24 Aug 2025 00:50:16 +0000 Subject: [PATCH 22/25] REFACTOR https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3217485418 --- infra/conf/vless.go | 64 +++-- main/commands/all/curve25519.go | 9 +- main/commands/all/mlkem768.go | 8 +- proxy/proxy.go | 13 +- proxy/vless/account.go | 6 +- proxy/vless/account.pb.go | 10 +- proxy/vless/account.proto | 2 +- proxy/vless/encryption/client.go | 358 ++++++++++++---------------- proxy/vless/encryption/common.go | 218 ++++++++++++----- proxy/vless/encryption/server.go | 388 ++++++++++++++----------------- proxy/vless/encryption/xor.go | 203 ++++------------ proxy/vless/inbound/config.pb.go | 12 +- proxy/vless/inbound/config.proto | 2 +- proxy/vless/inbound/inbound.go | 18 +- proxy/vless/outbound/outbound.go | 18 +- 15 files changed, 622 insertions(+), 707 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 44503912824e..b53208ddcd49 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -72,36 +72,35 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Decryption = c.Decryption if !func() bool { s := strings.Split(config.Decryption, ".") - if len(s) != 5 || s[2] != "mlkem768Seed" { + if len(s) < 4 || s[0] != "mlkem768x25519plus" { return false } - if s[0] != "1rtt" { - t := strings.TrimSuffix(s[0], "min") - if t == s[0] { - return false - } - i, err := strconv.Atoi(t) - if err != nil { - return false - } - config.Minutes = uint32(i) - } switch s[1] { case "native": - case "divide": + case "xorpub": config.XorMode = 1 case "random": config.XorMode = 2 default: return false } - if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { - 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) } - if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 64 { - return false + for i := 3; i < len(s); i++ { + if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 64 { + return false + } } - config.Decryption = s[4] + "." + s[3] + config.Decryption = config.Decryption[27+len(s[2]):] return true }() && config.Decryption != "none" { if config.Decryption == "" { @@ -220,36 +219,31 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { if !func() bool { s := strings.Split(account.Encryption, ".") - if len(s) != 5 || s[2] != "mlkem768Client" { + if len(s) < 4 || s[0] != "mlkem768x25519plus" { return false } - if s[0] != "1rtt" { - t := strings.TrimSuffix(s[0], "min") - if t == s[0] { - return false - } - i, err := strconv.Atoi(t) - if err != nil { - return false - } - account.Minutes = uint32(i) - } switch s[1] { case "native": - case "divide": + case "xorpub": account.XorMode = 1 case "random": account.XorMode = 2 default: return false } - if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { + switch s[2] { + case "1rtt": + case "0rtt": + account.Seconds = 1 + default: return false } - if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 1184 { - 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 = s[4] + "." + s[3] + account.Encryption = account.Encryption[27+len(s[2]):] return true }() && account.Encryption != "none" { if account.Encryption == "" { diff --git a/main/commands/all/curve25519.go b/main/commands/all/curve25519.go index c3c516ad526a..16ca8c7cf648 100644 --- a/main/commands/all/curve25519.go +++ b/main/commands/all/curve25519.go @@ -5,6 +5,8 @@ import ( "crypto/rand" "encoding/base64" "fmt" + + "lukechampine.com/blake3" ) func Curve25519Genkey(StdEncoding bool, input_base64 string) { @@ -40,7 +42,10 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { fmt.Println(err.Error()) return } - fmt.Printf("PrivateKey: %v\nPassword: %v", + password := key.PublicKey().Bytes() + hash32 := blake3.Sum256(password) + fmt.Printf("PrivateKey: %v\nPassword: %v\nHash32: %v", encoding.EncodeToString(privateKey), - encoding.EncodeToString(key.PublicKey().Bytes())) + encoding.EncodeToString(password), + encoding.EncodeToString(hash32[:])) } diff --git a/main/commands/all/mlkem768.go b/main/commands/all/mlkem768.go index f3cb0f79a095..0f6e707b53ab 100644 --- a/main/commands/all/mlkem768.go +++ b/main/commands/all/mlkem768.go @@ -3,11 +3,11 @@ package all import ( "crypto/mlkem" "crypto/rand" - "crypto/sha3" "encoding/base64" "fmt" "github.com/xtls/xray-core/main/commands/base" + "lukechampine.com/blake3" ) var cmdMLKEM768 = &base.Command{ @@ -42,9 +42,9 @@ func executeMLKEM768(cmd *base.Command, args []string) { } key, _ := mlkem.NewDecapsulationKey768(seed[:]) client := key.EncapsulationKey().Bytes() - hash32 := sha3.Sum256(client) - fmt.Printf("Seed: %v\nClient: %v\nHash11: %v", + hash32 := blake3.Sum256(client) + fmt.Printf("Seed: %v\nClient: %v\nHash32: %v", base64.RawURLEncoding.EncodeToString(seed[:]), base64.RawURLEncoding.EncodeToString(client), - base64.RawURLEncoding.EncodeToString(hash32[:11])) + base64.RawURLEncoding.EncodeToString(hash32[:])) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 8251849dba08..049d9fbdbb74 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -530,19 +530,12 @@ func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { var readCounter, writerCounter stats.Counter if conn != nil { isEncryption := false - if clientConn, ok := conn.(*encryption.ClientConn); ok { - conn = clientConn.Conn - isEncryption = true - } - if serverConn, ok := conn.(*encryption.ServerConn); ok { - conn = serverConn.Conn + if commonConn, ok := conn.(*encryption.CommonConn); ok { + conn = commonConn.Conn isEncryption = true } if xorConn, ok := conn.(*encryption.XorConn); ok { - if !xorConn.Divide { - return xorConn, nil, nil // full-random xorConn should not be penetrated - } - conn = xorConn.Conn + return xorConn, nil, nil // full-random xorConn should not be penetrated } if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection diff --git a/proxy/vless/account.go b/proxy/vless/account.go index 9967c7e1169f..b1e09619c2e4 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -19,7 +19,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? XorMode: a.XorMode, - Minutes: a.Minutes, + Seconds: a.Seconds, }, nil } @@ -32,7 +32,7 @@ type MemoryAccount struct { Encryption string XorMode uint32 - Minutes uint32 + Seconds uint32 } // Equals implements protocol.Account.Equals(). @@ -50,6 +50,6 @@ func (a *MemoryAccount) ToProto() proto.Message { Flow: a.Flow, Encryption: a.Encryption, XorMode: a.XorMode, - Minutes: a.Minutes, + Seconds: a.Seconds, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index ca638de11667..6048dc4e4358 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -31,7 +31,7 @@ type Account struct { 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"` - Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` + Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` } func (x *Account) Reset() { @@ -92,9 +92,9 @@ func (x *Account) GetXorMode() uint32 { return 0 } -func (x *Account) GetMinutes() uint32 { +func (x *Account) GetSeconds() uint32 { if x != nil { - return x.Minutes + return x.Seconds } return 0 } @@ -111,8 +111,8 @@ var file_proxy_vless_account_proto_rawDesc = []byte{ 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, 0x6d, 0x69, 0x6e, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, + 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, diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index f91a25281ec0..ebb1feff0fa4 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -14,5 +14,5 @@ message Account { string encryption = 3; uint32 xorMode = 4; - uint32 minutes = 5; + uint32 seconds = 5; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index a0ae0d2b763f..47a2408f1522 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -1,266 +1,216 @@ package encryption import ( - "bytes" "crypto/cipher" "crypto/ecdh" "crypto/mlkem" "crypto/rand" - "crypto/sha3" "io" "net" - "strings" "sync" "time" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/protocol" + "lukechampine.com/blake3" ) -var ClientCipher byte - -func init() { - if protocol.HasAESGCMHardwareSupport { - ClientCipher = 1 - } -} - type ClientInstance struct { - sync.RWMutex - nfsEKey *mlkem.EncapsulationKey768 - hash11 [11]byte // no more capacity - xorMode uint32 - xorPKey *ecdh.PublicKey - minutes time.Duration - expire time.Time - baseKey []byte - ticket []byte + NfsPKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + Seconds uint32 + + RWLock sync.RWMutex + Expire time.Time + PfsKey []byte + Ticket []byte } -type ClientConn struct { - net.Conn - instance *ClientInstance - baseKey []byte - ticket []byte - random []byte - aead cipher.AEAD - nonce []byte - peerAEAD cipher.AEAD - peerNonce []byte - PeerCache []byte -} - -func (i *ClientInstance) Init(nfsEKeyBytes, xorPKeyBytes []byte, xorMode, minutes uint32) (err error) { - if i.nfsEKey != nil { +func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) { + if i.NfsPKeys != nil { err = errors.New("already initialized") return } - if i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes); err != nil { + l := len(nfsPKeysBytes) + if l == 0 { + err = errors.New("empty nfsPKeysBytes") return } - if xorMode > 0 { - i.xorMode = xorMode - if i.xorPKey, err = ecdh.X25519().NewPublicKey(xorPKeyBytes); err != nil { - 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 } - hash32 := sha3.Sum256(nfsEKeyBytes) - copy(i.hash11[:], hash32[:]) + i.Hash32s[j] = blake3.Sum256(k) } - i.minutes = time.Duration(minutes) * time.Minute + i.RelaysLength -= 32 + i.XorMode = xorMode + i.Seconds = seconds return } -func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { - if i.nfsEKey == nil { +func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { + if i.NfsPKeys == nil { return nil, errors.New("uninitialized") } - if i.xorMode > 0 { - conn, _ = NewXorConn(conn, i.xorMode, i.xorPKey, nil) - } - c := &ClientConn{Conn: conn} - - if i.minutes > 0 { - i.RLock() - if time.Now().Before(i.expire) { - c.instance = i - c.baseKey = i.baseKey - c.ticket = i.ticket - i.RUnlock() + c := &CommonConn{Conn: 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 nfsPublicKey, 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) + nfsPublicKey = privateKey.PublicKey().Bytes() + copy(relays, nfsPublicKey) + var err error + nfsKey, err = privateKey.ECDH(k) + if err != nil { + return nil, err + } + } + if k, ok := k.(*mlkem.EncapsulationKey768); ok { + nfsKey, nfsPublicKey = k.Encapsulate() + copy(relays, nfsPublicKey) + index = 1088 + } + if i.XorMode > 0 { // this xor can (others can't) be decrypted by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, but it is not important + 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(nfsPublicKey, nfsKey) + + if i.Seconds > 0 { + i.RWLock.RLock() + if time.Now().Before(i.Expire) { + c.Client = i + c.UnitedKey = append(i.PfsKey, nfsKey...) + 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), 32) + } return c, nil } - i.RUnlock() + i.RWLock.RUnlock() } - pfsDKeySeed := make([]byte, 64) - rand.Read(pfsDKeySeed) - pfsDKey, _ := mlkem.NewDecapsulationKey768(pfsDKeySeed) - pfsEKeyBytes := pfsDKey.EncapsulationKey().Bytes() - nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate() - nfsAEAD := NewAEAD(ClientCipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey) + 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) - clientHello := make([]byte, 5+11+1+1184+1088+crypto.RandBetween(100, 1000)) - EncodeHeader(clientHello, 1, 11+1+1184+1088) - copy(clientHello[5:], i.hash11[:]) - clientHello[5+11] = ClientCipher - copy(clientHello[5+11+1:], pfsEKeyBytes) - copy(clientHello[5+11+1+1184:], encapsulatedNfsKey) - padding := clientHello[5+11+1+1184+1088:] - rand.Read(padding) // important - EncodeHeader(padding, 23, len(padding)-5) - nfsAEAD.Seal(padding[:5], clientHello[5:5+11+1], padding[5:len(padding)-16], padding[:5]) + 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 := c.Conn.Write(clientHello); err != nil { + if _, err := conn.Write(clientHello); err != nil { return nil, err } - // client can send more NFS AEAD paddings / messages if needed + // padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control - _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before server hello - if err != nil { + encryptedLength := make([]byte, 18) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } - - if t != 1 { - return nil, errors.New("unexpected type ", t, ", expect server hello") + if _, err := nfsGCM.Open(encryptedLength[:0], make([]byte, 12), encryptedLength, nil); err != nil { + return nil, err } - peerServerHello := make([]byte, 1088+21) - if l != len(peerServerHello) { - return nil, errors.New("unexpected length ", l, " for server hello") + length := DecodeLength(encryptedLength[:2]) + + if length < 1088+32+16 { // server may send more public keys + return nil, errors.New("too short length") } - if _, err := io.ReadFull(c.Conn, peerServerHello); err != nil { + encryptedPfsPublicKey := make([]byte, length) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } - encapsulatedPfsKey := peerServerHello[:1088] - c.ticket = append(i.hash11[:], peerServerHello[1088:]...) - - pfsKey, err := pfsDKey.Decapsulate(encapsulatedPfsKey) + nfsGCM.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) + mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088]) if err != nil { return nil, err } - c.baseKey = append(pfsKey, nfsKey...) - - VLESS, _ := NewAEAD(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) - if !bytes.Equal(VLESS, []byte("VLESS")) { - return nil, errors.New("invalid server").AtError() + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32]) + if err != nil { + return nil, err } - - if i.minutes > 0 { - i.Lock() - i.expire = time.Now().Add(i.minutes) - i.baseKey = c.baseKey - i.ticket = c.ticket - i.Unlock() + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) + if err != nil { + return nil, err } + pfsKey := append(mlkem768Key, x25519Key...) + c.UnitedKey = append(pfsKey, nfsKey...) + c.GCM = NewGCM(pfsPublicKey, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1088+32], c.UnitedKey) - return c, nil -} - -func (c *ClientConn) Write(b []byte) (int, error) { - if len(b) == 0 { - return 0, nil + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { + return nil, err } - var data []byte - for n := 0; n < len(b); { - b := b[n:] - if len(b) > 8192 { - b = b[:8192] // for avoiding another copy() in server's Read() - } - n += len(b) - if c.aead == nil { - data = make([]byte, 5+32+32+5+len(b)+16) - EncodeHeader(data, 0, 32+32) - copy(data[5:], c.ticket) - c.random = make([]byte, 32) - rand.Read(c.random) - copy(data[5+32:], c.random) - EncodeHeader(data[5+32+32:], 23, len(b)+16) - c.aead = NewAEAD(ClientCipher, c.baseKey, c.random, c.ticket) - c.nonce = make([]byte, 12) - c.aead.Seal(data[:5+32+32+5], c.nonce, b, data[5+32+32:5+32+32+5]) - } else { - data = make([]byte, 5+len(b)+16) - EncodeHeader(data, 23, len(b)+16) - c.aead.Seal(data[:5], c.nonce, b, data[:5]) - if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAEAD(ClientCipher, c.baseKey, data[5:], data[:5]) - } - } - IncreaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { - return 0, err - } + if _, err := c.PeerGCM.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { + return nil, err } - return len(b), nil -} + seconds := DecodeLength(encryptedTicket) -func (c *ClientConn) Read(b []byte) (int, error) { - if len(b) == 0 { - return 0, nil - } - if c.peerAEAD == nil { - _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before random hello - if err != nil { - if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT - c.instance.Lock() - if bytes.Equal(c.ticket, c.instance.ticket) { - c.instance.expire = time.Now() // expired - } - c.instance.Unlock() - return 0, errors.New("new handshake needed") - } - return 0, err - } - if t != 0 { - return 0, errors.New("unexpected type ", t, ", expect random hello") - } - peerRandomHello := make([]byte, 32) - if l != len(peerRandomHello) { - return 0, errors.New("unexpected length ", l, " for random hello") - } - if _, err := io.ReadFull(c.Conn, peerRandomHello); err != nil { - return 0, err - } - if c.random == nil { - return 0, errors.New("empty c.random") - } - c.peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random) - c.peerNonce = make([]byte, 12) - } - if len(c.PeerCache) != 0 { - n := copy(b, c.PeerCache) - c.PeerCache = c.PeerCache[n:] - return n, nil - } - h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 - if err != nil { - return 0, err - } - if t != 23 { - return 0, errors.New("unexpected type ", t, ", expect encrypted data") + 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() } - peerData := make([]byte, 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() + + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err } - var peerAEAD cipher.AEAD - if bytes.Equal(c.peerNonce, MaxNonce) { - peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerData, h) + if _, err := c.PeerGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err } - _, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h) - if peerAEAD != nil { - c.peerAEAD = peerAEAD + encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) // TODO: move to Read() + if _, err := io.ReadFull(conn, encryptedPadding); err != nil { + return nil, err } - IncreaseNonce(c.peerNonce) - if err != nil { - return 0, err + if _, err := c.PeerGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { + return nil, err } - if len(dst) > len(b) { - c.PeerCache = dst[copy(b, dst):] - dst = b // for len(dst) + + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, 0) } - return len(dst), nil + return c, nil } diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 4e2d475600ab..a2418c1f32b4 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -4,46 +4,175 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/hkdf" - "crypto/sha3" "fmt" "io" "net" + "strings" + "time" "github.com/xtls/xray-core/common/errors" - "golang.org/x/crypto/chacha20poly1305" + "lukechampine.com/blake3" ) -var MaxNonce = bytes.Repeat([]byte{255}, 12) +type CommonConn struct { + net.Conn + Client *ClientInstance + UnitedKey []byte + PreWrite []byte + GCM *GCM + PeerGCM *GCM + PeerCache []byte +} + +func (c *CommonConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + var data []byte + 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) + data = make([]byte, 5+len(b)+16) + EncodeHeader(data, len(b)+16) + aead := c.GCM + if bytes.Equal(c.GCM.Nonce[:], MaxNonce) { + aead = nil + } + c.GCM.Seal(data[:5], nil, b, data[:5]) + if aead == nil { + c.GCM = NewGCM(data[5:], c.UnitedKey) + } + if c.PreWrite != nil { + data = append(c.PreWrite, data...) + c.PreWrite = nil + } + if _, err := c.Conn.Write(data); 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, 32) + 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[16:]) + } + } + if len(c.PeerCache) != 0 { + n := copy(b, c.PeerCache) + c.PeerCache = c.PeerCache[n:] + return n, nil + } + h, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 + if err != nil { + if c.Client != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // client's 0-RTT + c.Client.RWLock.Lock() + if bytes.Equal(c.UnitedKey[:32], 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 := make([]byte, 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 peerAEAD *GCM + if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) { + peerAEAD = NewGCM(peerData, c.UnitedKey) + } + _, err = c.PeerGCM.Open(dst[:0], nil, peerData, h) + if peerAEAD != nil { + c.PeerGCM = peerAEAD + } + 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 EncodeHeader(h []byte, t byte, l int) { - switch t { - case 1: - h[0] = 1 - h[1] = 1 - h[2] = 1 - case 0: - h[0] = 0 - h[1] = 0 - h[2] = 0 - case 23: - h[0] = 23 - h[1] = 3 - h[2] = 3 +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) (t byte, l int, err error) { +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 { - t = 23 - } else if h[0] == 0 && h[1] == 0 && h[2] == 0 { - t = 0 - } else if h[0] == 1 && h[1] == 1 && h[2] == 1 { - t = 1 - } else { + if h[0] != 23 || h[1] != 3 || h[2] != 3 { l = 0 } if l < 17 || l > 17000 { // TODO: TLSv1.3 max length @@ -52,49 +181,22 @@ func DecodeHeader(h []byte) (t byte, l int, err error) { return } -func ReadAndDecodeHeader(conn net.Conn) (h []byte, t byte, l int, err error) { +func ReadAndDecodeHeader(conn net.Conn) (h []byte, l int, err error) { h = make([]byte, 5) if _, err = io.ReadFull(conn, h); err != nil { return } - t, l, err = DecodeHeader(h) + l, err = DecodeHeader(h) return } -func ReadAndDiscardPaddings(conn net.Conn, aead cipher.AEAD, nonce []byte) (h []byte, t byte, l int, err error) { +func ReadAndDiscardPaddings(conn net.Conn) (h []byte, l int, err error) { for { - if h, t, l, err = ReadAndDecodeHeader(conn); err != nil || t != 23 { + if h, l, err = ReadAndDecodeHeader(conn); err != nil { return } - padding := make([]byte, l) - if _, err = io.ReadFull(conn, padding); err != nil { + if _, err = io.ReadFull(conn, make([]byte, l)); err != nil { return } - if aead != nil { - if _, err := aead.Open(nil, nonce, padding, h); err != nil { - return h, t, l, err - } - IncreaseNonce(nonce) - } - } -} - -func NewAEAD(c byte, secret, salt, info []byte) (aead cipher.AEAD) { - key, _ := hkdf.Key(sha3.New256, secret, salt, string(info), 32) - if c&1 == 1 { - block, _ := aes.NewCipher(key) - aead, _ = cipher.NewGCM(block) - } else { - aead, _ = chacha20poly1305.New(key) - } - return -} - -func IncreaseNonce(nonce []byte) { - for i := range 12 { - nonce[11-i]++ - if nonce[11-i] != 0 { - break - } } } diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 336571b4ea49..4cf22516d4df 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -6,7 +6,6 @@ import ( "crypto/ecdh" "crypto/mlkem" "crypto/rand" - "crypto/sha3" "fmt" "io" "net" @@ -15,73 +14,77 @@ import ( "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" + "lukechampine.com/blake3" ) type ServerSession struct { - expire time.Time - cipher byte - baseKey []byte - randoms sync.Map + Expire time.Time + PfsKey []byte + Replays sync.Map } type ServerInstance struct { - sync.RWMutex - nfsDKey *mlkem.DecapsulationKey768 - hash11 [11]byte // no more capacity - xorMode uint32 - xorSKey *ecdh.PrivateKey - minutes time.Duration - sessions map[[32]byte]*ServerSession - closed bool -} + NfsSKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + Seconds uint32 -type ServerConn struct { - net.Conn - cipher byte - baseKey []byte - ticket []byte - peerRandom []byte - peerAEAD cipher.AEAD - peerNonce []byte - PeerCache []byte - aead cipher.AEAD - nonce []byte + RWLock sync.RWMutex + Sessions map[[16]byte]*ServerSession + Closed bool } -func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes uint32) (err error) { - if i.nfsDKey != nil { +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) { + if i.NfsSKeys != nil { err = errors.New("already initialized") return } - if i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed); err != nil { + l := len(nfsSKeysBytes) + if l == 0 { + err = errors.New("empty nfsSKeysBytes") return } - if xorMode > 0 { - i.xorMode = xorMode - if i.xorSKey, err = ecdh.X25519().NewPrivateKey(xorSKeyBytes); err != nil { - 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 } - hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - copy(i.hash11[:], hash32[:]) + i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j]) } - if minutes > 0 { - i.minutes = time.Duration(minutes) * time.Minute - i.sessions = make(map[[32]byte]*ServerSession) + 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.Lock() - if i.closed { - i.Unlock() + 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) + for ticket, session := range i.Sessions { + if now.After(session.Expire) { + delete(i.Sessions, ticket) } } - i.Unlock() + i.RWLock.Unlock() } }() } @@ -89,223 +92,190 @@ func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes } func (i *ServerInstance) Close() (err error) { - i.Lock() - i.closed = true - i.Unlock() + i.RWLock.Lock() + i.Closed = true + i.RWLock.Unlock() return } -func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { - if i.nfsDKey == nil { +func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { + if i.NfsSKeys == nil { return nil, errors.New("uninitialized") } - if i.xorMode > 0 { - var err error - if conn, err = NewXorConn(conn, i.xorMode, nil, i.xorSKey); err != nil { - return nil, err + c := &CommonConn{Conn: 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 nfsPublicKey, 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 :) + } + nfsPublicKey = relays[:index] + if k, ok := k.(*ecdh.PrivateKey); ok { + publicKey, err := ecdh.X25519().NewPublicKey(nfsPublicKey) + 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(nfsPublicKey) + 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:] } - c := &ServerConn{Conn: conn} + nfsGCM := NewGCM(nfsPublicKey, nfsKey) - _, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before client/ticket hello - if err != nil { + 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 t == 0 { - if i.minutes == 0 { + if length == 32 { + if i.Seconds == 0 { return nil, errors.New("0-RTT is not allowed") } - peerTicketHello := make([]byte, 32+32) - if l != len(peerTicketHello) { - return nil, errors.New("unexpected length ", l, " for ticket hello") - } - if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { return nil, err } - if !bytes.Equal(peerTicketHello[:11], i.hash11[:]) { - return nil, errors.New("unexpected hash11: ", fmt.Sprintf("%v", peerTicketHello[:11])) + ticket, err := nfsGCM.Open(nil, nil, encryptedTicket, nil) + if err != nil { + return nil, err } - i.RLock() - s := i.sessions[[32]byte(peerTicketHello)] - i.RUnlock() + i.RWLock.RLock() + s := i.Sessions[[16]byte(ticket)] + i.RWLock.RUnlock() if s == nil { noises := make([]byte, crypto.RandBetween(100, 1000)) var err error for err == nil { rand.Read(noises) - _, _, err = DecodeHeader(noises) + _, err = DecodeHeader(noises) } - c.Conn.Write(noises) // make client do new handshake + conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } - if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[32:]), true); replay { + if _, replay := s.Replays.LoadOrStore([32]byte(encryptedTicket), true); replay { return nil, errors.New("replay detected") } - c.cipher = s.cipher - c.baseKey = s.baseKey - c.ticket = peerTicketHello[:32] - c.peerRandom = peerTicketHello[32:] + c.UnitedKey = append(s.PfsKey, nfsKey...) // the same key links the upload & download + c.PreWrite = make([]byte, 32) // always trust yourself, not the client + rand.Read(c.PreWrite) + c.GCM = NewGCM(c.PreWrite, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite[16:]), NewCTR(c.UnitedKey, iv), 32, 0) + } return c, nil } - peerClientHello := make([]byte, 11+1+1184+1088) - if l != len(peerClientHello) { - return nil, errors.New("unexpected length ", l, " for client hello") + if length < 1184+32+16 { // client may send more public keys + return nil, errors.New("too short length") } - if _, err := io.ReadFull(c.Conn, peerClientHello); err != nil { + encryptedPfsPublicKey := make([]byte, length) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } - if !bytes.Equal(peerClientHello[:11], i.hash11[:]) { - return nil, errors.New("unexpected hash11: ", fmt.Sprintf("%v", peerClientHello[:11])) - } - c.cipher = peerClientHello[11] - pfsEKeyBytes := peerClientHello[11+1 : 11+1+1184] - encapsulatedNfsKey := peerClientHello[11+1+1184:] - - pfsEKey, err := mlkem.NewEncapsulationKey768(pfsEKeyBytes) - if err != nil { + if _, err := nfsGCM.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { return nil, err } - nfsKey, err := i.nfsDKey.Decapsulate(encapsulatedNfsKey) + mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184]) if err != nil { return nil, err } - nfsAEAD := NewAEAD(c.cipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey) - nfsNonce := append([]byte{}, peerClientHello[:11+1]...) - pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() - c.baseKey = append(pfsKey, nfsKey...) - pfsAEAD := NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey) - pfsNonce := append([]byte{}, peerClientHello[:11+1]...) - c.ticket = append(i.hash11[:], pfsAEAD.Seal(nil, pfsNonce, []byte("VLESS"), pfsEKeyBytes)...) - IncreaseNonce(pfsNonce) - - serverHello := make([]byte, 5+1088+21+crypto.RandBetween(100, 1000)) - EncodeHeader(serverHello, 1, 1088+21) - copy(serverHello[5:], encapsulatedPfsKey) - copy(serverHello[5+1088:], c.ticket[11:]) - padding := serverHello[5+1088+21:] - rand.Read(padding) // important - EncodeHeader(padding, 23, len(padding)-5) - pfsAEAD.Seal(padding[:5], pfsNonce, padding[5:len(padding)-16], padding[:5]) - - if _, err := c.Conn.Write(serverHello); err != nil { + mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate() + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32]) + if err != nil { return nil, err } - // server can send more PFS AEAD paddings / messages if needed - - _, t, l, err = ReadAndDiscardPaddings(c.Conn, nfsAEAD, nfsNonce) // allow paddings before ticket hello + x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) if err != nil { return nil, err } - if t != 0 { - return nil, errors.New("unexpected type ", t, ", expect ticket hello") - } - peerTicketHello := make([]byte, 32+32) - if l != len(peerTicketHello) { - return nil, errors.New("unexpected length ", l, " for ticket hello") - } - if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil { + pfsKey := append(mlkem768Key, 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 := 18 + 1088 + 32 + 16 + encryptedTicketLength := 32 + paddingLength := int(crypto.RandBetween(100, 1000)) + serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) + nfsGCM.Seal(serverHello[:0], make([]byte, 12), EncodeLength(pfsKeyExchangeLength-18), nil) // it is safe because our nonce starts from 1 + nfsGCM.Seal(serverHello[:18], 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 } - if !bytes.Equal(peerTicketHello[:32], c.ticket) { - return nil, errors.New("naughty boy") - } - c.peerRandom = peerTicketHello[32:] + // padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control - if i.minutes > 0 { - i.Lock() - s := &ServerSession{ - expire: time.Now().Add(i.minutes), - cipher: c.cipher, - baseKey: c.baseKey, + 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, } - s.randoms.Store([32]byte(c.peerRandom), true) - i.sessions[[32]byte(c.ticket)] = s - i.Unlock() + i.RWLock.Unlock() } - return c, nil -} - -func (c *ServerConn) Read(b []byte) (int, error) { - if len(b) == 0 { - return 0, nil - } - if c.peerAEAD == nil { - c.peerAEAD = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) - c.peerNonce = make([]byte, 12) - } - if len(c.PeerCache) != 0 { - n := copy(b, c.PeerCache) - c.PeerCache = c.PeerCache[n:] - return n, nil - } - h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 - if err != nil { - return 0, err - } - if t != 23 { - return 0, errors.New("unexpected type ", t, ", expect encrypted data") - } - peerData := make([]byte, 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 peerAEAD cipher.AEAD - if bytes.Equal(c.peerNonce, MaxNonce) { - peerAEAD = NewAEAD(c.cipher, c.baseKey, peerData, h) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err } - _, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h) - if peerAEAD != nil { - c.peerAEAD = peerAEAD + if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err } - IncreaseNonce(c.peerNonce) - if err != nil { - return 0, err + encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) + if _, err := io.ReadFull(conn, encryptedPadding); err != nil { + return nil, err } - if len(dst) > len(b) { - c.PeerCache = dst[copy(b, dst):] - dst = b // for len(dst) + if _, err := nfsGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { + return nil, err } - return len(dst), nil -} -func (c *ServerConn) Write(b []byte) (int, error) { - if len(b) == 0 { - return 0, nil + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0) } - var data []byte - for n := 0; n < len(b); { - b := b[n:] - if len(b) > 8192 { - b = b[:8192] // for avoiding another copy() in client's Read() - } - n += len(b) - if c.aead == nil { - data = make([]byte, 5+32+5+len(b)+16) - EncodeHeader(data, 0, 32) - rand.Read(data[5 : 5+32]) - EncodeHeader(data[5+32:], 23, len(b)+16) - c.aead = NewAEAD(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) - c.nonce = make([]byte, 12) - c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5]) - } else { - data = make([]byte, 5+len(b)+16) - EncodeHeader(data, 23, len(b)+16) - c.aead.Seal(data[:5], c.nonce, b, data[:5]) - if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAEAD(c.cipher, c.baseKey, data[5:], data[:5]) - } - } - IncreaseNonce(c.nonce) - if _, err := c.Conn.Write(data); err != nil { - return 0, err - } - } - return len(b), nil + return c, nil } diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index c5586ae915ed..e435cb5ce880 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -3,135 +3,61 @@ package encryption import ( "crypto/aes" "crypto/cipher" - "crypto/ecdh" - "crypto/hkdf" - "crypto/rand" - "crypto/sha3" - "io" "net" - "github.com/xtls/xray-core/common/errors" + "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 - Divide bool - - head []byte - key []byte - ctr cipher.Stream - peerCtr cipher.Stream - isHeader bool - skipNext bool - - out_after0 bool - out_header []byte - out_skip int - - in_after0 bool - in_header []byte - in_skip int + CTR cipher.Stream + PeerCTR cipher.Stream + OutSkip int + OutHeader []byte + InSkip int + InHeader []byte } -func NewCTR(key, iv []byte, isServer bool) cipher.Stream { - info := "CLIENT" - if isServer { - info = "SERVER" // avoids attackers sending traffic back to the client, though the encryption layer has its own protection +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 } - key, _ = hkdf.Key(sha3.New256, key, iv, info, 32) // avoids using pKey directly if attackers sent the basepoint, or whaterver they like - block, _ := aes.NewCipher(key) - return cipher.NewCTR(block, iv) } -func NewXorConn(conn net.Conn, mode uint32, pKey *ecdh.PublicKey, sKey *ecdh.PrivateKey) (*XorConn, error) { - if mode == 0 || (pKey == nil && sKey == nil) || (pKey != nil && sKey != nil) { - return nil, errors.New("invalid parameters") - } - c := &XorConn{ - Conn: conn, - Divide: mode == 1, - isHeader: true, - out_header: make([]byte, 0, 5), // important - in_header: make([]byte, 0, 5), // important - } - if pKey != nil { - c.head = make([]byte, 16+32) - rand.Read(c.head) - eSKey, _ := ecdh.X25519().NewPrivateKey(c.head[16:]) - NewCTR(pKey.Bytes(), c.head[:16], false).XORKeyStream(c.head[16:], eSKey.PublicKey().Bytes()) // make X25519 public key distinguishable from random bytes - c.key, _ = eSKey.ECDH(pKey) - c.ctr = NewCTR(c.key, c.head[:16], false) - } - if sKey != nil { - peerHead := make([]byte, 16+32) - if _, err := io.ReadFull(c.Conn, peerHead); err != nil { - return nil, err - } - NewCTR(sKey.PublicKey().Bytes(), peerHead[:16], false).XORKeyStream(peerHead[16:], peerHead[16:]) // we don't use buggy elligator, because we have PSK :) - ePKey, err := ecdh.X25519().NewPublicKey(peerHead[16:]) - if err != nil { - return nil, err - } - key, err := sKey.ECDH(ePKey) - if err != nil { - return nil, err - } - c.peerCtr = NewCTR(key, peerHead[:16], false) - c.head = make([]byte, 16) - rand.Read(c.head) // make sure the server always replies random bytes even when received replays, though it is not important - c.ctr = NewCTR(key, c.head, true) // the same key links the upload & download, though the encryption layer has its own link - } - return c, nil - //chacha20.NewUnauthenticatedCipher() -} - -func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records +func (c *XorConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - if !c.out_after0 { - t, l, _ := DecodeHeader(b) - if t == 23 { // single 23 - l = 5 - } else { // 1/0 + 23, or noises only - l += 10 - if t == 0 { - c.out_after0 = true - if c.Divide { - l -= 5 - } - } - } - c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b - l = len(b) - if c.head != nil { - b = append(c.head, b...) - c.head = nil - } - if _, err := c.Conn.Write(b); err != nil { - return 0, err - } - return l, nil - } - if c.Divide { - return c.Conn.Write(b) - } - for p := b; ; { // for XTLS - if len(p) <= c.out_skip { - c.out_skip -= len(p) + for p := b; ; { + if len(p) <= c.OutSkip { + c.OutSkip -= len(p) break } - p = p[c.out_skip:] - c.out_skip = 0 - need := 5 - len(c.out_header) + p = p[c.OutSkip:] + c.OutSkip = 0 + need := 5 - len(c.OutHeader) if len(p) < need { - c.out_header = append(c.out_header, p...) - c.ctr.XORKeyStream(p, p) + c.OutHeader = append(c.OutHeader, p...) + c.CTR.XORKeyStream(p, p) break } - _, c.out_skip, _ = DecodeHeader(append(c.out_header, p[:need]...)) - c.out_header = c.out_header[:0] - c.ctr.XORKeyStream(p[:need], p[:need]) + 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 { @@ -140,60 +66,27 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records return len(b), nil } -func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... +func (c *XorConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - if !c.in_after0 || !c.isHeader { - if c.peerCtr == nil { // for client - peerIv := make([]byte, 16) - if _, err := io.ReadFull(c.Conn, peerIv); err != nil { - return 0, err - } - c.peerCtr = NewCTR(c.key, peerIv, true) - } - if _, err := io.ReadFull(c.Conn, b); err != nil { - return 0, err - } - if c.skipNext { - c.skipNext = false - return len(b), nil - } - c.peerCtr.XORKeyStream(b, b) - if c.isHeader { // always 5-bytes - if t, _, _ := DecodeHeader(b); t == 23 { - c.skipNext = true - } else { - c.isHeader = false - if t == 0 { - c.in_after0 = true - } - } - } else { - c.isHeader = true - } - return len(b), nil - } - if c.Divide { - return c.Conn.Read(b) - } n, err := c.Conn.Read(b) - for p := b[:n]; ; { // for XTLS - if len(p) <= c.in_skip { - c.in_skip -= len(p) + for p := b[:n]; ; { + if len(p) <= c.InSkip { + c.InSkip -= len(p) break } - p = p[c.in_skip:] - c.in_skip = 0 - need := 5 - len(c.in_header) + p = p[c.InSkip:] + c.InSkip = 0 + need := 5 - len(c.InHeader) if len(p) < need { - c.peerCtr.XORKeyStream(p, p) - c.in_header = append(c.in_header, p...) + c.PeerCTR.XORKeyStream(p, p) + c.InHeader = append(c.InHeader, p...) break } - c.peerCtr.XORKeyStream(p[:need], p[:need]) - _, c.in_skip, _ = DecodeHeader(append(c.in_header, p[:need]...)) - c.in_header = c.in_header[:0] + 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 240c25d99c52..e3192cf8ec73 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -115,7 +115,7 @@ 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"` - Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` + Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` } func (x *Config) Reset() { @@ -176,9 +176,9 @@ func (x *Config) GetXorMode() uint32 { return 0 } -func (x *Config) GetMinutes() uint32 { +func (x *Config) GetSeconds() uint32 { if x != nil { - return x.Minutes + return x.Seconds } return 0 } @@ -211,9 +211,9 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 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, 0x6d, 0x69, - 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, - 0x75, 0x74, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, + 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, diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto index 186d8588feb3..e1ebc8d37059 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -23,5 +23,5 @@ message Config { string decryption = 3; uint32 xorMode = 4; - uint32 minutes = 5; + uint32 seconds = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index fc8dd243e6e8..2a25dc5c61c5 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -84,12 +84,16 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } - if s := strings.Split(config.Decryption, "."); len(s) == 2 { - nfsDKeySeed, _ := base64.RawURLEncoding.DecodeString(s[0]) - xorSKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) + if 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(nfsDKeySeed, xorSKeyBytes, config.XorMode, config.Minutes); err != nil { - return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() + if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.Seconds); err != nil { + return nil, errors.New("failed to use decryption").Base(err).AtError() } } @@ -498,9 +502,9 @@ 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.ServerConn); ok { + if serverConn, ok := connection.(*encryption.CommonConn); ok { peerCache = &serverConn.PeerCache - if xorConn, ok := serverConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + 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 diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 519748254f30..4611750ef0a9 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -69,12 +69,16 @@ func New(ctx context.Context, config *Config) (*Handler, error) { } a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) - if s := strings.Split(a.Encryption, "."); len(s) == 2 { - nfsEKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[0]) - xorPKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) + if 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(nfsEKeyBytes, xorPKeyBytes, a.XorMode, a.Minutes); err != nil { - return nil, errors.New("failed to use mlkem768client").Base(err).AtError() + if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds); err != nil { + return nil, errors.New("failed to use encryption").Base(err).AtError() } } @@ -161,9 +165,9 @@ 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.ClientConn); ok { + if clientConn, ok := conn.(*encryption.CommonConn); ok { peerCache = &clientConn.PeerCache - if xorConn, ok := clientConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + 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 From 0199dea39988a1a1b846d0bf8598631bade40902 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:30:11 +0000 Subject: [PATCH 23/25] Ready to release https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3218219079 --- proxy/vless/encryption/client.go | 46 +++++++++++--------------------- proxy/vless/encryption/common.go | 40 ++++++++++++++++----------- proxy/vless/encryption/server.go | 27 +++++++++---------- proxy/vless/inbound/inbound.go | 2 +- proxy/vless/outbound/outbound.go | 2 +- 5 files changed, 55 insertions(+), 62 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 47a2408f1522..fbd32b5a35fa 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -76,14 +76,13 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { iv := clientHello[:16] rand.Read(iv) relays := clientHello[16:ivAndRealysLength] - var nfsPublicKey, nfsKey []byte + 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) - nfsPublicKey = privateKey.PublicKey().Bytes() - copy(relays, nfsPublicKey) + copy(relays, privateKey.PublicKey().Bytes()) var err error nfsKey, err = privateKey.ECDH(k) if err != nil { @@ -91,11 +90,12 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { } } if k, ok := k.(*mlkem.EncapsulationKey768); ok { - nfsKey, nfsPublicKey = k.Encapsulate() - copy(relays, nfsPublicKey) + var ciphertext []byte + nfsKey, ciphertext = k.Encapsulate() + copy(relays, ciphertext) index = 1088 } - if i.XorMode > 0 { // this xor can (others can't) be decrypted by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, but it is not important + 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 { @@ -108,20 +108,20 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:]) relays = relays[index+32:] } - nfsGCM := NewGCM(nfsPublicKey, nfsKey) + 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...) + 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), 32) + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16) } return c, nil } @@ -142,21 +142,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if _, err := conn.Write(clientHello); err != nil { return nil, err } - // padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control + // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control - encryptedLength := make([]byte, 18) - if _, err := io.ReadFull(conn, encryptedLength); err != nil { - return nil, err - } - if _, err := nfsGCM.Open(encryptedLength[:0], make([]byte, 12), encryptedLength, nil); err != nil { - return nil, err - } - length := DecodeLength(encryptedLength[:2]) - - if length < 1088+32+16 { // server may send more public keys - return nil, errors.New("too short length") - } - encryptedPfsPublicKey := make([]byte, length) + encryptedPfsPublicKey := make([]byte, 1088+32+16) if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } @@ -195,22 +183,18 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { 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 } - encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) // TODO: move to Read() - if _, err := io.ReadFull(conn, encryptedPadding); err != nil { - return nil, err - } - if _, err := c.PeerGCM.Open(encryptedPadding[:0], nil, encryptedPadding, 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, 0) + 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 index a2418c1f32b4..8c3d17b7a32a 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -16,12 +16,13 @@ import ( type CommonConn struct { net.Conn - Client *ClientInstance - UnitedKey []byte - PreWrite []byte - GCM *GCM - PeerGCM *GCM - PeerCache []byte + Client *ClientInstance + UnitedKey []byte + PreWrite []byte + GCM *GCM + PeerGCM *GCM + PeerPadding []byte + PeerCache []byte } func (c *CommonConn) Write(b []byte) (int, error) { @@ -37,12 +38,12 @@ func (c *CommonConn) Write(b []byte) (int, error) { n += len(b) data = make([]byte, 5+len(b)+16) EncodeHeader(data, len(b)+16) - aead := c.GCM + max := false if bytes.Equal(c.GCM.Nonce[:], MaxNonce) { - aead = nil + max = true } c.GCM.Seal(data[:5], nil, b, data[:5]) - if aead == nil { + if max { c.GCM = NewGCM(data[5:], c.UnitedKey) } if c.PreWrite != nil { @@ -61,15 +62,24 @@ func (c *CommonConn) Read(b []byte) (int, error) { return 0, nil } if c.PeerGCM == nil { // client's 0-RTT - serverRandom := make([]byte, 32) + 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[16:]) + 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:] @@ -96,13 +106,13 @@ func (c *CommonConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } - var peerAEAD *GCM + var newGCM *GCM if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) { - peerAEAD = NewGCM(peerData, c.UnitedKey) + newGCM = NewGCM(peerData, c.UnitedKey) } _, err = c.PeerGCM.Open(dst[:0], nil, peerData, h) - if peerAEAD != nil { - c.PeerGCM = peerAEAD + if newGCM != nil { + c.PeerGCM = newGCM } if err != nil { return 0, err diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 4cf22516d4df..2256c994f405 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -110,7 +110,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { } iv := ivAndRelays[:16] relays := ivAndRelays[16:] - var nfsPublicKey, nfsKey []byte + var nfsKey []byte var lastCTR cipher.Stream for j, k := range i.NfsSKeys { if lastCTR != nil { @@ -123,9 +123,8 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { if i.XorMode > 0 { NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator, because we have PSK :) } - nfsPublicKey = relays[:index] if k, ok := k.(*ecdh.PrivateKey); ok { - publicKey, err := ecdh.X25519().NewPublicKey(nfsPublicKey) + publicKey, err := ecdh.X25519().NewPublicKey(relays[:index]) if err != nil { return nil, err } @@ -136,7 +135,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { } if k, ok := k.(*mlkem.DecapsulationKey768); ok { var err error - nfsKey, err = k.Decapsulate(nfsPublicKey) + nfsKey, err = k.Decapsulate(relays[:index]) if err != nil { return nil, err } @@ -152,7 +151,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { } relays = relays[32:] } - nfsGCM := NewGCM(nfsPublicKey, nfsKey) + nfsGCM := NewGCM(iv, nfsKey) encryptedLength := make([]byte, 18) if _, err := io.ReadFull(conn, encryptedLength); err != nil { @@ -188,16 +187,16 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } - if _, replay := s.Replays.LoadOrStore([32]byte(encryptedTicket), true); replay { + if _, replay := s.Replays.LoadOrStore([32]byte(nfsKey), true); replay { // prevents bad client also return nil, errors.New("replay detected") } - c.UnitedKey = append(s.PfsKey, nfsKey...) // the same key links the upload & download - c.PreWrite = make([]byte, 32) // always trust yourself, not the client + c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download + c.PreWrite = make([]byte, 16) // always trust yourself, not the client rand.Read(c.PreWrite) c.GCM = NewGCM(c.PreWrite, c.UnitedKey) - c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) // unchangeable ctx, and different ctx length for upload / download if i.XorMode == 2 { - c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite[16:]), NewCTR(c.UnitedKey, iv), 32, 0) + 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 } @@ -235,12 +234,11 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { rand.Read(ticket) copy(ticket, EncodeLength(int(i.Seconds*4/5))) - pfsKeyExchangeLength := 18 + 1088 + 32 + 16 + pfsKeyExchangeLength := 1088 + 32 + 16 encryptedTicketLength := 32 paddingLength := int(crypto.RandBetween(100, 1000)) serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) - nfsGCM.Seal(serverHello[:0], make([]byte, 12), EncodeLength(pfsKeyExchangeLength-18), nil) // it is safe because our nonce starts from 1 - nfsGCM.Seal(serverHello[:18], MaxNonce, pfsPublicKey, nil) + 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) @@ -249,7 +247,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { if _, err := conn.Write(serverHello); err != nil { return nil, err } - // padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control + // 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() @@ -260,6 +258,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { 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 } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 2a25dc5c61c5..5143d8684851 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -84,7 +84,7 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } - if config.Decryption != "none" { + if config.Decryption != "" && config.Decryption != "none" { s := strings.Split(config.Decryption, ".") var nfsSKeysBytes [][]byte for _, r := range s { diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 4611750ef0a9..ee1b6dfb4caa 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -69,7 +69,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) { } a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) - if a.Encryption != "none" { + if a.Encryption != "" && a.Encryption != "none" { s := strings.Split(a.Encryption, ".") var nfsPKeysBytes [][]byte for _, r := range s { From fce1195b60f48ca18a953dbd5c7d991869de9a5e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:18:26 +0000 Subject: [PATCH 24/25] Final changes https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3218352839 --- proxy/vless/encryption/client.go | 4 +++- proxy/vless/encryption/common.go | 2 +- proxy/vless/encryption/server.go | 20 +++++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index fbd32b5a35fa..832fb31a5a31 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -161,7 +161,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if err != nil { return nil, err } - pfsKey := append(mlkem768Key, x25519Key...) + 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) diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 8c3d17b7a32a..9ab91188ea70 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -89,7 +89,7 @@ func (c *CommonConn) Read(b []byte) (int, error) { if err != nil { if c.Client != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // client's 0-RTT c.Client.RWLock.Lock() - if bytes.Equal(c.UnitedKey[:32], c.Client.PfsKey) { + if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) { c.Client.Expire = time.Now() // expired } c.Client.RWLock.Unlock() diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 2256c994f405..33894312222b 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -20,7 +20,7 @@ import ( type ServerSession struct { Expire time.Time PfsKey []byte - Replays sync.Map + NfsKeys sync.Map } type ServerInstance struct { @@ -178,7 +178,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { s := i.Sessions[[16]byte(ticket)] i.RWLock.RUnlock() if s == nil { - noises := make([]byte, crypto.RandBetween(100, 1000)) + 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) @@ -187,21 +187,21 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { conn.Write(noises) // make client do new handshake return nil, errors.New("expired ticket") } - if _, replay := s.Replays.LoadOrStore([32]byte(nfsKey), true); replay { // prevents bad client also + 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 - c.PreWrite = make([]byte, 16) // always trust yourself, not the client - rand.Read(c.PreWrite) + 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, and different ctx length for upload / download + 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 + 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) @@ -225,7 +225,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { if err != nil { return nil, err } - pfsKey := append(mlkem768Key, x25519Key...) + 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) From b0b220985c9c1bc832665458d5fd6e0c287b67ae Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:47:14 +0000 Subject: [PATCH 25/25] Changes before merging https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3229878125 --- proxy/vless/encryption/client.go | 2 +- proxy/vless/encryption/common.go | 66 ++++++++++++++++---------------- proxy/vless/encryption/server.go | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 832fb31a5a31..301c23280dfa 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -66,7 +66,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if i.NfsPKeys == nil { return nil, errors.New("uninitialized") } - c := &CommonConn{Conn: conn} + c := NewCommonConn(conn) ivAndRealysLength := 16 + i.RelaysLength pfsKeyExchangeLength := 18 + 1184 + 32 + 16 diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index 9ab91188ea70..6f914c8db774 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -8,12 +8,19 @@ import ( "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 @@ -22,35 +29,44 @@ type CommonConn struct { 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 } - var data []byte + 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) - data = make([]byte, 5+len(b)+16) - EncodeHeader(data, len(b)+16) + 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(data[:5], nil, b, data[:5]) + c.GCM.Seal(headerAndData[:5], nil, b, headerAndData[:5]) if max { - c.GCM = NewGCM(data[5:], c.UnitedKey) + c.GCM = NewGCM(headerAndData, c.UnitedKey) } if c.PreWrite != nil { - data = append(c.PreWrite, data...) + headerAndData = append(c.PreWrite, headerAndData...) c.PreWrite = nil } - if _, err := c.Conn.Write(data); err != nil { + if _, err := c.Conn.Write(headerAndData); err != nil { return 0, err } } @@ -80,14 +96,18 @@ func (c *CommonConn) Read(b []byte) (int, error) { } c.PeerPadding = nil } - if len(c.PeerCache) != 0 { + if len(c.PeerCache) > 0 { n := copy(b, c.PeerCache) c.PeerCache = c.PeerCache[n:] return n, nil } - h, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 + 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.HasPrefix(err.Error(), "invalid header: ") { // client's 0-RTT + 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 @@ -98,7 +118,7 @@ func (c *CommonConn) Read(b []byte) (int, error) { return 0, err } c.Client = nil - peerData := make([]byte, l) + peerData := c.PeerInBytes[5 : 5+l] if _, err := io.ReadFull(c.Conn, peerData); err != nil { return 0, err } @@ -108,9 +128,9 @@ func (c *CommonConn) Read(b []byte) (int, error) { } var newGCM *GCM if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) { - newGCM = NewGCM(peerData, c.UnitedKey) + newGCM = NewGCM(c.PeerInBytes[:5+l], c.UnitedKey) } - _, err = c.PeerGCM.Open(dst[:0], nil, peerData, h) + _, err = c.PeerGCM.Open(dst[:0], nil, peerData, peerHeader) if newGCM != nil { c.PeerGCM = newGCM } @@ -190,23 +210,3 @@ func DecodeHeader(h []byte) (l int, err error) { } return } - -func ReadAndDecodeHeader(conn net.Conn) (h []byte, l int, err error) { - h = make([]byte, 5) - if _, err = io.ReadFull(conn, h); err != nil { - return - } - l, err = DecodeHeader(h) - return -} - -func ReadAndDiscardPaddings(conn net.Conn) (h []byte, l int, err error) { - for { - if h, l, err = ReadAndDecodeHeader(conn); err != nil { - return - } - if _, err = io.ReadFull(conn, make([]byte, l)); err != nil { - return - } - } -} diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 33894312222b..8594fd252776 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -102,7 +102,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { if i.NfsSKeys == nil { return nil, errors.New("uninitialized") } - c := &CommonConn{Conn: conn} + c := NewCommonConn(conn) ivAndRelays := make([]byte, 16+i.RelaysLength) if _, err := io.ReadFull(conn, ivAndRelays); err != nil {