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] 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 261e21d9..fb785d73 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 5d4ace6f..ed090fac 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 3667a1d8..9f8270f9 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 fe0f5eb4..495fb088 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 00000000..78512bad --- /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 b01e88f0..1fe27bf5 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 70da4668..1de0e515 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 73f669b2..607562b6 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 c22cfe16..71b2f274 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 fd5d4518..be718d29 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 51d2cb7d..1b82a836 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 00000000..425b1d00 --- /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 00000000..07edeae9 --- /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 00000000..7e7819f7 --- /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 907a3f7f..20837a4c 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 94b5551c..dc6bbac5 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 df1f9f3c..83af17e8 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 e1a727eb..5193e60a 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