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 1a7341b3..5d10cc97 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -1,6 +1,7 @@ package conf import ( + "encoding/base64" "encoding/json" "path/filepath" "runtime" @@ -80,10 +81,45 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Clients[idx] = user } - if c.Decryption != "none" { - return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`) - } config.Decryption = c.Decryption + if !func() bool { + s := strings.Split(config.Decryption, ".") + if len(s) < 4 || s[0] != "mlkem768x25519plus" { + return false + } + switch s[1] { + case "native": + case "xorpub": + config.XorMode = 1 + case "random": + config.XorMode = 2 + default: + return false + } + if s[2] != "1rtt" { + t := strings.TrimSuffix(s[2], "s") + if t == s[2] { + return false + } + i, err := strconv.Atoi(t) + if err != nil { + return false + } + config.Seconds = uint32(i) + } + for i := 3; i < len(s); i++ { + if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 64 { + return false + } + } + config.Decryption = config.Decryption[27+len(s[2]):] + return true + }() && config.Decryption != "none" { + if config.Decryption == "" { + return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`) + } + return nil, errors.New(`VLESS settings: unsupported "decryption": ` + config.Decryption) + } for _, fb := range c.Fallbacks { var i uint16 @@ -155,16 +191,16 @@ type VLessOutboundConfig struct { func (c *VLessOutboundConfig) Build() (proto.Message, error) { config := new(outbound.Config) - if len(c.Vnext) == 0 { - return nil, errors.New(`VLESS settings: "vnext" is empty`) + if len(c.Vnext) != 1 { + return nil, errors.New(`VLESS settings: "vnext" should have one and only one member`) } config.Vnext = make([]*protocol.ServerEndpoint, len(c.Vnext)) for idx, rec := range c.Vnext { if rec.Address == nil { return nil, errors.New(`VLESS vnext: "address" is not set`) } - if len(rec.Users) == 0 { - return nil, errors.New(`VLESS vnext: "users" is empty`) + if len(rec.Users) != 1 { + return nil, errors.New(`VLESS vnext: "users" should have one and only one member`) } spec := &protocol.ServerEndpoint{ Address: rec.Address.Build(), @@ -193,8 +229,39 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) } - if account.Encryption != "none" { - return nil, errors.New(`VLESS users: please add/set "encryption":"none" for every user`) + if !func() bool { + s := strings.Split(account.Encryption, ".") + if len(s) < 4 || s[0] != "mlkem768x25519plus" { + return false + } + switch s[1] { + case "native": + case "xorpub": + account.XorMode = 1 + case "random": + account.XorMode = 2 + default: + return false + } + switch s[2] { + case "1rtt": + case "0rtt": + account.Seconds = 1 + default: + return false + } + for i := 3; i < len(s); i++ { + if b, _ := base64.RawURLEncoding.DecodeString(s[i]); len(b) != 32 && len(b) != 1184 { + return false + } + } + account.Encryption = account.Encryption[27+len(s[2]):] + return true + }() && account.Encryption != "none" { + if account.Encryption == "" { + return nil, errors.New(`VLESS users: please add/set "encryption":"none" for every user`) + } + return nil, errors.New(`VLESS users: unsupported "encryption": ` + account.Encryption) } user.Account = serial.ToTypedMessage(account) diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index 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/curve25519.go b/main/commands/all/curve25519.go index bb706c6c..16ca8c7c 100644 --- a/main/commands/all/curve25519.go +++ b/main/commands/all/curve25519.go @@ -1,17 +1,15 @@ package all import ( + "crypto/ecdh" "crypto/rand" "encoding/base64" "fmt" - "golang.org/x/crypto/curve25519" + "lukechampine.com/blake3" ) func Curve25519Genkey(StdEncoding bool, input_base64 string) { - var output string - var err error - var privateKey, publicKey []byte var encoding *base64.Encoding if *input_stdEncoding || StdEncoding { encoding = base64.StdEncoding @@ -19,40 +17,35 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { encoding = base64.RawURLEncoding } + var privateKey []byte if len(input_base64) > 0 { - privateKey, err = encoding.DecodeString(input_base64) - if err != nil { - output = err.Error() - goto out - } - if len(privateKey) != curve25519.ScalarSize { - output = "Invalid length of private key." - goto out + privateKey, _ = encoding.DecodeString(input_base64) + if len(privateKey) != 32 { + fmt.Println("Invalid length of X25519 private key.") + return } } - if privateKey == nil { - privateKey = make([]byte, curve25519.ScalarSize) - if _, err = rand.Read(privateKey); err != nil { - output = err.Error() - goto out - } + privateKey = make([]byte, 32) + rand.Read(privateKey) } // Modify random bytes using algorithm described at: - // https://cr.yp.to/ecdh.html. + // https://cr.yp.to/ecdh.html + // (Just to make sure printing the real private key) privateKey[0] &= 248 privateKey[31] &= 127 privateKey[31] |= 64 - if publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint); err != nil { - output = err.Error() - goto out + key, err := ecdh.X25519().NewPrivateKey(privateKey) + if err != nil { + fmt.Println(err.Error()) + return } - - output = fmt.Sprintf("Private key: %v\nPublic key: %v", + password := key.PublicKey().Bytes() + hash32 := blake3.Sum256(password) + fmt.Printf("PrivateKey: %v\nPassword: %v\nHash32: %v", encoding.EncodeToString(privateKey), - encoding.EncodeToString(publicKey)) -out: - fmt.Println(output) + encoding.EncodeToString(password), + encoding.EncodeToString(hash32[:])) } diff --git a/main/commands/all/mldsa65.go b/main/commands/all/mldsa65.go index 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..0f6e707b --- /dev/null +++ b/main/commands/all/mlkem768.go @@ -0,0 +1,50 @@ +package all + +import ( + "crypto/mlkem" + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/xtls/xray-core/main/commands/base" + "lukechampine.com/blake3" +) + +var cmdMLKEM768 = &base.Command{ + UsageLine: `{{.Exec}} mlkem768 [-i "seed (base64.RawURLEncoding)"]`, + Short: `Generate key pair for ML-KEM-768 post-quantum key exchange (VLESS)`, + Long: ` +Generate key pair for ML-KEM-768 post-quantum key exchange (VLESS). + +Random: {{.Exec}} mlkem768 + +From seed: {{.Exec}} mlkem768 -i "seed (base64.RawURLEncoding)" +`, +} + +func init() { + cmdMLKEM768.Run = executeMLKEM768 // break init loop +} + +var input_mlkem768 = cmdMLKEM768.Flag.String("i", "", "") + +func executeMLKEM768(cmd *base.Command, args []string) { + var seed [64]byte + if len(*input_mlkem768) > 0 { + s, _ := base64.RawURLEncoding.DecodeString(*input_mlkem768) + if len(s) != 64 { + fmt.Println("Invalid length of ML-KEM-768 seed.") + return + } + seed = [64]byte(s) + } else { + rand.Read(seed[:]) + } + key, _ := mlkem.NewDecapsulationKey768(seed[:]) + client := key.EncapsulationKey().Bytes() + hash32 := blake3.Sum256(client) + fmt.Printf("Seed: %v\nClient: %v\nHash32: %v", + base64.RawURLEncoding.EncodeToString(seed[:]), + base64.RawURLEncoding.EncodeToString(client), + base64.RawURLEncoding.EncodeToString(hash32[:])) +} diff --git a/main/commands/all/uuid.go b/main/commands/all/uuid.go index 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..7ef23f03 100644 --- a/main/commands/all/x25519.go +++ b/main/commands/all/x25519.go @@ -6,9 +6,9 @@ import ( var cmdX25519 = &base.Command{ UsageLine: `{{.Exec}} x25519 [-i "private key (base64.RawURLEncoding)"] [--std-encoding]`, - Short: `Generate key pair for x25519 key exchange`, + Short: `Generate key pair for X25519 key exchange (VLESS, REALITY)`, Long: ` -Generate key pair for x25519 key exchange. +Generate key pair for X25519 key exchange (VLESS, REALITY). Random: {{.Exec}} x25519 diff --git a/proxy/proxy.go b/proxy/proxy.go index 3fec31af..049d9fbd 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -25,6 +25,7 @@ import ( "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy/vless/encryption" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/reality" @@ -524,24 +525,33 @@ func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx conte } } -// UnwrapRawConn support unwrap stats, tls, utls, reality, proxyproto, uds-wrapper conn and get raw tcp/uds conn from it +// UnwrapRawConn support unwrap encryption, stats, tls, utls, reality, proxyproto, uds-wrapper conn and get raw tcp/uds conn from it func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { var readCounter, writerCounter stats.Counter if conn != nil { - statConn, ok := conn.(*stat.CounterConnection) - if ok { + isEncryption := false + if commonConn, ok := conn.(*encryption.CommonConn); ok { + conn = commonConn.Conn + isEncryption = true + } + if xorConn, ok := conn.(*encryption.XorConn); ok { + return xorConn, nil, nil // full-random xorConn should not be penetrated + } + if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection readCounter = statConn.ReadCounter writerCounter = statConn.WriteCounter } - if xc, ok := conn.(*tls.Conn); ok { - conn = xc.NetConn() - } else if utlsConn, ok := conn.(*tls.UConn); ok { - conn = utlsConn.NetConn() - } else if realityConn, ok := conn.(*reality.Conn); ok { - conn = realityConn.NetConn() - } else if realityUConn, ok := conn.(*reality.UConn); ok { - conn = realityUConn.NetConn() + if !isEncryption { // avoids double penetration + if xc, ok := conn.(*tls.Conn); ok { + conn = xc.NetConn() + } else if utlsConn, ok := conn.(*tls.UConn); ok { + conn = utlsConn.NetConn() + } else if realityConn, ok := conn.(*reality.Conn); ok { + conn = realityConn.NetConn() + } else if realityUConn, ok := conn.(*reality.UConn); ok { + conn = realityUConn.NetConn() + } } if pc, ok := conn.(*proxyproto.Conn); ok { conn = pc.Raw() @@ -632,9 +642,20 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net } func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, readCounter stats.Counter) error { - errors.LogInfo(ctx, "CopyRawConn readv") + errors.LogInfo(ctx, "CopyRawConn (maybe) readv") if err := buf.Copy(reader, writer, buf.UpdateActivity(timer), buf.AddToStatCounter(readCounter)); err != nil { return errors.New("failed to process response").Base(err) } return nil } + +func IsRAWTransport(conn stat.Connection) bool { + iConn := conn + if statConn, ok := iConn.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + _, ok1 := iConn.(*proxyproto.Conn) + _, ok2 := iConn.(*net.TCPConn) + _, ok3 := iConn.(*internet.UnixConnWrapper) + return ok1 || ok2 || ok3 +} diff --git a/proxy/vless/account.go b/proxy/vless/account.go index c22cfe16..b1e09619 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,6 +18,8 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? + XorMode: a.XorMode, + Seconds: a.Seconds, }, nil } @@ -27,8 +29,10 @@ type MemoryAccount struct { ID *protocol.ID // Flow of the account. May be "xtls-rprx-vision". Flow string - // Encryption of the account. Used for client connections, and only accepts "none" for now. + Encryption string + XorMode uint32 + Seconds uint32 } // Equals implements protocol.Account.Equals(). @@ -45,5 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, + XorMode: a.XorMode, + Seconds: a.Seconds, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index fd5d4518..6048dc4e 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -28,9 +28,10 @@ type Account struct { // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Flow settings. May be "xtls-rprx-vision". - Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` - // Encryption settings. Only applies to client side, and only accepts "none" for now. + Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` + Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` } func (x *Account) Reset() { @@ -84,23 +85,40 @@ func (x *Account) GetEncryption() string { return "" } +func (x *Account) GetXorMode() uint32 { + if x != nil { + return x.XorMode + } + return 0 +} + +func (x *Account) GetSeconds() uint32 { + if x != nil { + return x.Seconds + } + return 0 +} + var File_proxy_vless_account_proto protoreflect.FileDescriptor var file_proxy_vless_account_proto_rawDesc = []byte{ 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x4d, 0x0a, - 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, 0x0a, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x52, 0x0a, 0x14, - 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, - 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, - 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x81, 0x01, + 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, + 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, + 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, + 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x6f, 0x6e, + 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, + 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, + 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 51d2cb7d..ebb1feff 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -11,6 +11,8 @@ message Account { string id = 1; // Flow settings. May be "xtls-rprx-vision". string flow = 2; - // Encryption settings. Only applies to client side, and only accepts "none" for now. + string encryption = 3; + uint32 xorMode = 4; + uint32 seconds = 5; } diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go index 61bfb911..1176a960 100644 --- a/proxy/vless/encoding/encoding.go +++ b/proxy/vless/encoding/encoding.go @@ -172,7 +172,7 @@ func DecodeResponseHeader(reader io.Reader, request *protocol.RequestHeader) (*A } // XtlsRead filter and read xtls protocol -func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, trafficState *proxy.TrafficState, ob *session.Outbound, isUplink bool, ctx context.Context) error { +func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, conn net.Conn, peerCache *[]byte, input *bytes.Reader, rawInput *bytes.Buffer, trafficState *proxy.TrafficState, ob *session.Outbound, isUplink bool, ctx context.Context) error { err := func() error { for { if isUplink && trafficState.Inbound.UplinkReaderDirectCopy || !isUplink && trafficState.Outbound.DownlinkReaderDirectCopy { @@ -194,15 +194,21 @@ func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, if !buffer.IsEmpty() { timer.Update() if isUplink && trafficState.Inbound.UplinkReaderDirectCopy || !isUplink && trafficState.Outbound.DownlinkReaderDirectCopy { - // XTLS Vision processes struct TLS Conn's input and rawInput - if inputBuffer, err := buf.ReadFrom(input); err == nil { - if !inputBuffer.IsEmpty() { - buffer, _ = buf.MergeMulti(buffer, inputBuffer) + // XTLS Vision processes struct Encryption Conn's peerCache or TLS Conn's input and rawInput + if peerCache != nil { + if len(*peerCache) != 0 { + buffer = buf.MergeBytes(buffer, *peerCache) } - } - if rawInputBuffer, err := buf.ReadFrom(rawInput); err == nil { - if !rawInputBuffer.IsEmpty() { - buffer, _ = buf.MergeMulti(buffer, rawInputBuffer) + } else { + if inputBuffer, err := buf.ReadFrom(input); err == nil { + if !inputBuffer.IsEmpty() { + buffer, _ = buf.MergeMulti(buffer, inputBuffer) + } + } + if rawInputBuffer, err := buf.ReadFrom(rawInput); err == nil { + if !rawInputBuffer.IsEmpty() { + buffer, _ = buf.MergeMulti(buffer, rawInputBuffer) + } } } } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go new file mode 100644 index 00000000..301c2328 --- /dev/null +++ b/proxy/vless/encryption/client.go @@ -0,0 +1,202 @@ +package encryption + +import ( + "crypto/cipher" + "crypto/ecdh" + "crypto/mlkem" + "crypto/rand" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "lukechampine.com/blake3" +) + +type ClientInstance struct { + NfsPKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + Seconds uint32 + + RWLock sync.RWMutex + Expire time.Time + PfsKey []byte + Ticket []byte +} + +func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) { + if i.NfsPKeys != nil { + err = errors.New("already initialized") + return + } + l := len(nfsPKeysBytes) + if l == 0 { + err = errors.New("empty nfsPKeysBytes") + return + } + i.NfsPKeys = make([]any, l) + i.NfsPKeysBytes = nfsPKeysBytes + i.Hash32s = make([][32]byte, l) + for j, k := range nfsPKeysBytes { + if len(k) == 32 { + if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil { + return + } + i.RelaysLength += 32 + 32 + } else { + if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil { + return + } + i.RelaysLength += 1088 + 32 + } + i.Hash32s[j] = blake3.Sum256(k) + } + i.RelaysLength -= 32 + i.XorMode = xorMode + i.Seconds = seconds + return +} + +func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { + if i.NfsPKeys == nil { + return nil, errors.New("uninitialized") + } + c := NewCommonConn(conn) + + ivAndRealysLength := 16 + i.RelaysLength + pfsKeyExchangeLength := 18 + 1184 + 32 + 16 + paddingLength := int(crypto.RandBetween(100, 1000)) + clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength) + + iv := clientHello[:16] + rand.Read(iv) + relays := clientHello[16:ivAndRealysLength] + var nfsKey []byte + var lastCTR cipher.Stream + for j, k := range i.NfsPKeys { + var index = 32 + if k, ok := k.(*ecdh.PublicKey); ok { + privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + copy(relays, privateKey.PublicKey().Bytes()) + var err error + nfsKey, err = privateKey.ECDH(k) + if err != nil { + return nil, err + } + } + if k, ok := k.(*mlkem.EncapsulationKey768); ok { + var ciphertext []byte + nfsKey, ciphertext = k.Encapsulate() + copy(relays, ciphertext) + index = 1088 + } + if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values + NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes + } + if lastCTR != nil { + lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable + } + if j == len(i.NfsPKeys)-1 { + break + } + lastCTR = NewCTR(nfsKey, iv) + lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:]) + relays = relays[index+32:] + } + nfsGCM := NewGCM(iv, nfsKey) + + if i.Seconds > 0 { + i.RWLock.RLock() + if time.Now().Before(i.Expire) { + c.Client = i + c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection + nfsGCM.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil) + nfsGCM.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil) + i.RWLock.RUnlock() + c.PreWrite = clientHello[:ivAndRealysLength+18+32] + c.GCM = NewGCM(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey) + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16) + } + return c, nil + } + i.RWLock.RUnlock() + } + + pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength] + nfsGCM.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil) + mlkem768DKey, _ := mlkem.GenerateKey768() + x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...) + nfsGCM.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil) + + padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:] + nfsGCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + nfsGCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) + + if _, err := conn.Write(clientHello); err != nil { + return nil, err + } + // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + + encryptedPfsPublicKey := make([]byte, 1088+32+16) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { + return nil, err + } + nfsGCM.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) + mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088]) + if err != nil { + return nil, err + } + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32]) + if err != nil { + return nil, err + } + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) + if err != nil { + return nil, err + } + pfsKey := make([]byte, 32+32) // no more capacity + copy(pfsKey, mlkem768Key) + copy(pfsKey[32:], x25519Key) + c.UnitedKey = append(pfsKey, nfsKey...) + c.GCM = NewGCM(pfsPublicKey, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1088+32], c.UnitedKey) + + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { + return nil, err + } + if _, err := c.PeerGCM.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { + return nil, err + } + seconds := DecodeLength(encryptedTicket) + + if i.Seconds > 0 && seconds > 0 { + i.RWLock.Lock() + i.Expire = time.Now().Add(time.Duration(seconds) * time.Second) + i.PfsKey = pfsKey + i.Ticket = encryptedTicket[:16] + i.RWLock.Unlock() + } + + encryptedLength := make([]byte, 18) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if _, err := c.PeerGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + length := DecodeLength(encryptedLength[:2]) + c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern + + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length) + } + return c, nil +} diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go new file mode 100644 index 00000000..6f914c8d --- /dev/null +++ b/proxy/vless/encryption/common.go @@ -0,0 +1,212 @@ +package encryption + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" + "io" + "net" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/errors" + "lukechampine.com/blake3" +) + +var OutBytesPool = sync.Pool{ + New: func() any { + return make([]byte, 5+8192+16) + }, +} + +type CommonConn struct { + net.Conn + Client *ClientInstance + UnitedKey []byte + PreWrite []byte + GCM *GCM + PeerGCM *GCM + PeerPadding []byte + PeerInBytes []byte + PeerCache []byte +} + +func NewCommonConn(conn net.Conn) *CommonConn { + return &CommonConn{ + Conn: conn, + PeerInBytes: make([]byte, 5+17000), // no need to use sync.Pool, because we are always reading + } +} + +func (c *CommonConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + outBytes := OutBytesPool.Get().([]byte) + defer OutBytesPool.Put(outBytes) + for n := 0; n < len(b); { + b := b[n:] + if len(b) > 8192 { + b = b[:8192] // for avoiding another copy() in peer's Read() + } + n += len(b) + headerAndData := outBytes[:5+len(b)+16] + EncodeHeader(headerAndData, len(b)+16) + max := false + if bytes.Equal(c.GCM.Nonce[:], MaxNonce) { + max = true + } + c.GCM.Seal(headerAndData[:5], nil, b, headerAndData[:5]) + if max { + c.GCM = NewGCM(headerAndData, c.UnitedKey) + } + if c.PreWrite != nil { + headerAndData = append(c.PreWrite, headerAndData...) + c.PreWrite = nil + } + if _, err := c.Conn.Write(headerAndData); err != nil { + return 0, err + } + } + return len(b), nil +} + +func (c *CommonConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + if c.PeerGCM == nil { // client's 0-RTT + serverRandom := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, serverRandom); err != nil { + return 0, err + } + c.PeerGCM = NewGCM(serverRandom, c.UnitedKey) + if xorConn, ok := c.Conn.(*XorConn); ok { + xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom) + } + } + if c.PeerPadding != nil { // client's 1-RTT + if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil { + return 0, err + } + if _, err := c.PeerGCM.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil { + return 0, err + } + c.PeerPadding = nil + } + if len(c.PeerCache) > 0 { + n := copy(b, c.PeerCache) + c.PeerCache = c.PeerCache[n:] + return n, nil + } + peerHeader := c.PeerInBytes[:5] + if _, err := io.ReadFull(c.Conn, peerHeader); err != nil { + return 0, err + } + l, err := DecodeHeader(c.PeerInBytes[:5]) // l: 17~17000 + if err != nil { + if c.Client != nil && strings.Contains(err.Error(), "invalid header: ") { // client's 0-RTT + c.Client.RWLock.Lock() + if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) { + c.Client.Expire = time.Now() // expired + } + c.Client.RWLock.Unlock() + return 0, errors.New("new handshake needed") + } + return 0, err + } + c.Client = nil + peerData := c.PeerInBytes[5 : 5+l] + if _, err := io.ReadFull(c.Conn, peerData); err != nil { + return 0, err + } + dst := peerData[:l-16] + if len(dst) <= len(b) { + dst = b[:len(dst)] // avoids another copy() + } + var newGCM *GCM + if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) { + newGCM = NewGCM(c.PeerInBytes[:5+l], c.UnitedKey) + } + _, err = c.PeerGCM.Open(dst[:0], nil, peerData, peerHeader) + if newGCM != nil { + c.PeerGCM = newGCM + } + if err != nil { + return 0, err + } + if len(dst) > len(b) { + c.PeerCache = dst[copy(b, dst):] + dst = b // for len(dst) + } + return len(dst), nil +} + +type GCM struct { + cipher.AEAD + Nonce [12]byte +} + +func NewGCM(ctx, key []byte) *GCM { + k := make([]byte, 32) + blake3.DeriveKey(k, string(ctx), key) + block, _ := aes.NewCipher(k) + aead, _ := cipher.NewGCM(block) + return &GCM{AEAD: aead} + //chacha20poly1305.New() +} + +func (a *GCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if nonce == nil { + nonce = IncreaseNonce(a.Nonce[:]) + } + return a.AEAD.Seal(dst, nonce, plaintext, additionalData) +} + +func (a *GCM) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + if nonce == nil { + nonce = IncreaseNonce(a.Nonce[:]) + } + return a.AEAD.Open(dst, nonce, ciphertext, additionalData) +} + +func IncreaseNonce(nonce []byte) []byte { + for i := range 12 { + nonce[11-i]++ + if nonce[11-i] != 0 { + break + } + } + return nonce +} + +var MaxNonce = bytes.Repeat([]byte{255}, 12) + +func EncodeLength(l int) []byte { + return []byte{byte(l >> 8), byte(l)} +} + +func DecodeLength(b []byte) int { + return int(b[0])<<8 | int(b[1]) +} + +func EncodeHeader(h []byte, l int) { + h[0] = 23 + h[1] = 3 + h[2] = 3 + h[3] = byte(l >> 8) + h[4] = byte(l) +} + +func DecodeHeader(h []byte) (l int, err error) { + l = int(h[3])<<8 | int(h[4]) + if h[0] != 23 || h[1] != 3 || h[2] != 3 { + l = 0 + } + if l < 17 || l > 17000 { // TODO: TLSv1.3 max length + err = errors.New("invalid header: ", fmt.Sprintf("%v", h[:5])) // DO NOT CHANGE: relied by client's Read() + } + return +} diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go new file mode 100644 index 00000000..8594fd25 --- /dev/null +++ b/proxy/vless/encryption/server.go @@ -0,0 +1,282 @@ +package encryption + +import ( + "bytes" + "crypto/cipher" + "crypto/ecdh" + "crypto/mlkem" + "crypto/rand" + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "lukechampine.com/blake3" +) + +type ServerSession struct { + Expire time.Time + PfsKey []byte + NfsKeys sync.Map +} + +type ServerInstance struct { + NfsSKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + Seconds uint32 + + RWLock sync.RWMutex + Sessions map[[16]byte]*ServerSession + Closed bool +} + +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) { + if i.NfsSKeys != nil { + err = errors.New("already initialized") + return + } + l := len(nfsSKeysBytes) + if l == 0 { + err = errors.New("empty nfsSKeysBytes") + return + } + i.NfsSKeys = make([]any, l) + i.NfsPKeysBytes = make([][]byte, l) + i.Hash32s = make([][32]byte, l) + for j, k := range nfsSKeysBytes { + if len(k) == 32 { + if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil { + return + } + i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes() + i.RelaysLength += 32 + 32 + } else { + if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil { + return + } + i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes() + i.RelaysLength += 1088 + 32 + } + i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j]) + } + i.RelaysLength -= 32 + i.XorMode = xorMode + if seconds > 0 { + i.Seconds = seconds + i.Sessions = make(map[[16]byte]*ServerSession) + go func() { + for { + time.Sleep(time.Minute) + i.RWLock.Lock() + if i.Closed { + i.RWLock.Unlock() + return + } + now := time.Now() + for ticket, session := range i.Sessions { + if now.After(session.Expire) { + delete(i.Sessions, ticket) + } + } + i.RWLock.Unlock() + } + }() + } + return +} + +func (i *ServerInstance) Close() (err error) { + i.RWLock.Lock() + i.Closed = true + i.RWLock.Unlock() + return +} + +func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { + if i.NfsSKeys == nil { + return nil, errors.New("uninitialized") + } + c := NewCommonConn(conn) + + ivAndRelays := make([]byte, 16+i.RelaysLength) + if _, err := io.ReadFull(conn, ivAndRelays); err != nil { + return nil, err + } + iv := ivAndRelays[:16] + relays := ivAndRelays[16:] + var nfsKey []byte + var lastCTR cipher.Stream + for j, k := range i.NfsSKeys { + if lastCTR != nil { + lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay + } + var index = 32 + if _, ok := k.(*mlkem.DecapsulationKey768); ok { + index = 1088 + } + if i.XorMode > 0 { + NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator, because we have PSK :) + } + if k, ok := k.(*ecdh.PrivateKey); ok { + publicKey, err := ecdh.X25519().NewPublicKey(relays[:index]) + if err != nil { + return nil, err + } + nfsKey, err = k.ECDH(publicKey) + if err != nil { + return nil, err + } + } + if k, ok := k.(*mlkem.DecapsulationKey768); ok { + var err error + nfsKey, err = k.Decapsulate(relays[:index]) + if err != nil { + return nil, err + } + } + if j == len(i.NfsSKeys)-1 { + break + } + relays = relays[index:] + lastCTR = NewCTR(nfsKey, iv) + lastCTR.XORKeyStream(relays, relays[:32]) + if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) { + return nil, errors.New("unexpected hash32: ", fmt.Sprintf("%v", relays[:32])) + } + relays = relays[32:] + } + nfsGCM := NewGCM(iv, nfsKey) + + encryptedLength := make([]byte, 18) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + length := DecodeLength(encryptedLength[:2]) + + if length == 32 { + if i.Seconds == 0 { + return nil, errors.New("0-RTT is not allowed") + } + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { + return nil, err + } + ticket, err := nfsGCM.Open(nil, nil, encryptedTicket, nil) + if err != nil { + return nil, err + } + i.RWLock.RLock() + s := i.Sessions[[16]byte(ticket)] + i.RWLock.RUnlock() + if s == nil { + noises := make([]byte, crypto.RandBetween(1268, 2268)) // matches 1-RTT's server hello length for "random", though it is not important, just for example + var err error + for err == nil { + rand.Read(noises) + _, err = DecodeHeader(noises) + } + conn.Write(noises) // make client do new handshake + return nil, errors.New("expired ticket") + } + if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also + return nil, errors.New("replay detected") + } + c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request) + c.PreWrite = make([]byte, 16) + rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub") + c.GCM = NewGCM(c.PreWrite, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client) + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client + } + return c, nil + } + + if length < 1184+32+16 { // client may send more public keys in the future's version + return nil, errors.New("too short length") + } + encryptedPfsPublicKey := make([]byte, length) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { + return nil, err + } + if _, err := nfsGCM.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { + return nil, err + } + mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184]) + if err != nil { + return nil, err + } + mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate() + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32]) + if err != nil { + return nil, err + } + x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) + if err != nil { + return nil, err + } + pfsKey := make([]byte, 32+32) // no more capacity + copy(pfsKey, mlkem768Key) + copy(pfsKey[32:], x25519Key) + pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...) + c.UnitedKey = append(pfsKey, nfsKey...) + c.GCM = NewGCM(pfsPublicKey, c.UnitedKey) + c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1184+32], c.UnitedKey) + ticket := make([]byte, 16) + rand.Read(ticket) + copy(ticket, EncodeLength(int(i.Seconds*4/5))) + + pfsKeyExchangeLength := 1088 + 32 + 16 + encryptedTicketLength := 32 + paddingLength := int(crypto.RandBetween(100, 1000)) + serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) + nfsGCM.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil) + c.GCM.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket, nil) + padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:] + c.GCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + c.GCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) + + if _, err := conn.Write(serverHello); err != nil { + return nil, err + } + // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + + if i.Seconds > 0 { + i.RWLock.Lock() + i.Sessions[[16]byte(ticket)] = &ServerSession{ + Expire: time.Now().Add(time.Duration(i.Seconds) * time.Second), + PfsKey: pfsKey, + } + i.RWLock.Unlock() + } + + // important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) + if _, err := io.ReadFull(conn, encryptedPadding); err != nil { + return nil, err + } + if _, err := nfsGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { + return nil, err + } + + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0) + } + return c, nil +} diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go new file mode 100644 index 00000000..e435cb5c --- /dev/null +++ b/proxy/vless/encryption/xor.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "net" + + "lukechampine.com/blake3" +) + +func NewCTR(key, iv []byte) cipher.Stream { + k := make([]byte, 32) + blake3.DeriveKey(k, "VLESS", key) // avoids using key directly + block, _ := aes.NewCipher(k) + return cipher.NewCTR(block, iv) + //chacha20.NewUnauthenticatedCipher() +} + +type XorConn struct { + net.Conn + CTR cipher.Stream + PeerCTR cipher.Stream + OutSkip int + OutHeader []byte + InSkip int + InHeader []byte +} + +func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn { + return &XorConn{ + Conn: conn, + CTR: ctr, + PeerCTR: peerCTR, + OutSkip: outSkip, + OutHeader: make([]byte, 0, 5), // important + InSkip: inSkip, + InHeader: make([]byte, 0, 5), // important + } +} + +func (c *XorConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + for p := b; ; { + if len(p) <= c.OutSkip { + c.OutSkip -= len(p) + break + } + p = p[c.OutSkip:] + c.OutSkip = 0 + need := 5 - len(c.OutHeader) + if len(p) < need { + c.OutHeader = append(c.OutHeader, p...) + c.CTR.XORKeyStream(p, p) + break + } + c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...)) + c.OutHeader = c.OutHeader[:0] + c.CTR.XORKeyStream(p[:need], p[:need]) + p = p[need:] + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *XorConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + n, err := c.Conn.Read(b) + for p := b[:n]; ; { + if len(p) <= c.InSkip { + c.InSkip -= len(p) + break + } + p = p[c.InSkip:] + c.InSkip = 0 + need := 5 - len(c.InHeader) + if len(p) < need { + c.PeerCTR.XORKeyStream(p, p) + c.InHeader = append(c.InHeader, p...) + break + } + c.PeerCTR.XORKeyStream(p[:need], p[:need]) + c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...)) + c.InHeader = c.InHeader[:0] + p = p[need:] + } + return n, err +} diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index 907a3f7f..e3192cf8 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -111,11 +111,11 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` - // Decryption settings. Only applies to server side, and only accepts "none" - // for now. - Decryption string `protobuf:"bytes,2,opt,name=decryption,proto3" json:"decryption,omitempty"` - Fallbacks []*Fallback `protobuf:"bytes,3,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` + Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` + Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` + Decryption string `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` + Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` } func (x *Config) Reset() { @@ -155,6 +155,13 @@ func (x *Config) GetClients() []*protocol.User { return nil } +func (x *Config) GetFallbacks() []*Fallback { + if x != nil { + return x.Fallbacks + } + return nil +} + func (x *Config) GetDecryption() string { if x != nil { return x.Decryption @@ -162,11 +169,18 @@ func (x *Config) GetDecryption() string { return "" } -func (x *Config) GetFallbacks() []*Fallback { +func (x *Config) GetXorMode() uint32 { if x != nil { - return x.Fallbacks + return x.XorMode } - return nil + return 0 +} + +func (x *Config) GetSeconds() uint32 { + if x != nil { + return x.Seconds + } + return 0 } var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor @@ -185,25 +199,28 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xa0, 0x01, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xd4, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1e, - 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, - 0x0a, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x40, + 0x0a, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, - 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, - 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, - 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x18, 0x0a, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x65, 0x63, + 0x6f, 0x6e, 0x64, 0x73, 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto index 94b5551c..e1ebc8d3 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -19,8 +19,9 @@ message Fallback { message Config { repeated xray.common.protocol.User clients = 1; - // Decryption settings. Only applies to server side, and only accepts "none" - // for now. - string decryption = 2; - repeated Fallback fallbacks = 3; + repeated Fallback fallbacks = 2; + + string decryption = 3; + uint32 xorMode = 4; + uint32 seconds = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index dfa8d470..ca5176b9 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -4,6 +4,7 @@ import ( "bytes" "context" gotls "crypto/tls" + "encoding/base64" "io" "reflect" "strconv" @@ -29,6 +30,7 @@ import ( "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless/encoding" + "github.com/xtls/xray-core/proxy/vless/encryption" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -67,6 +69,7 @@ type Handler struct { policyManager policy.Manager validator vless.Validator dns dns.Client + decryption *encryption.ServerInstance fallbacks map[string]map[string]map[string]*Fallback // or nil // regexps map[string]*regexp.Regexp // or nil } @@ -81,6 +84,19 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } + if config.Decryption != "" && config.Decryption != "none" { + s := strings.Split(config.Decryption, ".") + var nfsSKeysBytes [][]byte + for _, r := range s { + b, _ := base64.RawURLEncoding.DecodeString(r) + nfsSKeysBytes = append(nfsSKeysBytes, b) + } + handler.decryption = &encryption.ServerInstance{} + if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.Seconds); err != nil { + return nil, errors.New("failed to use decryption").Base(err).AtError() + } + } + if config.Fallbacks != nil { handler.fallbacks = make(map[string]map[string]map[string]*Fallback) // handler.regexps = make(map[string]*regexp.Regexp) @@ -159,6 +175,9 @@ func isMuxAndNotXUDP(request *protocol.RequestHeader, first *buf.Buffer) bool { // Close implements common.Closable.Close(). func (h *Handler) Close() error { + if h.decryption != nil { + h.decryption.Close() + } return errors.Combine(common.Close(h.validator)) } @@ -199,6 +218,14 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s iConn = statConn.Connection } + if h.decryption != nil { + var err error + connection, err = h.decryption.Handshake(connection) + if err != nil { + return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo() + } + } + sessionPolicy := h.policyManager.ForLevel(0) if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { return errors.New("unable to set read deadline").Base(err).AtWarning() @@ -464,6 +491,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s // Flow: requestAddons.Flow, } + var peerCache *[]byte var input *bytes.Reader var rawInput *bytes.Buffer switch requestAddons.Flow { @@ -476,6 +504,13 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s case protocol.RequestCommandMux: fallthrough // we will break Mux connections that contain TCP requests case protocol.RequestCommandTCP: + if serverConn, ok := connection.(*encryption.CommonConn); ok { + peerCache = &serverConn.PeerCache + if _, ok := serverConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) { + inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice + } + break + } var t reflect.Type var p uintptr if tlsConn, ok := iConn.(*tls.Conn); ok { @@ -544,7 +579,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s if requestAddons.Flow == vless.XRV { ctx1 := session.ContextWithInbound(ctx, nil) // TODO enable splice clientReader = proxy.NewVisionReader(clientReader, trafficState, true, ctx1) - err = encoding.XtlsRead(clientReader, serverWriter, timer, connection, input, rawInput, trafficState, nil, true, ctx1) + err = encoding.XtlsRead(clientReader, serverWriter, timer, connection, peerCache, input, rawInput, trafficState, nil, true, ctx1) } else { // from clientReader.ReadMultiBuffer to serverWriter.WriteMultiBuffer err = buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer)) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index e1a727eb..ee1b6dfb 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -4,7 +4,9 @@ import ( "bytes" "context" gotls "crypto/tls" + "encoding/base64" "reflect" + "strings" "time" "unsafe" @@ -24,6 +26,7 @@ import ( "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless/encoding" + "github.com/xtls/xray-core/proxy/vless/encryption" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/reality" @@ -43,6 +46,7 @@ type Handler struct { serverPicker protocol.ServerPicker policyManager policy.Manager cone bool + encryption *encryption.ClientInstance } // New creates a new VLess outbound handler. @@ -64,6 +68,20 @@ func New(ctx context.Context, config *Config) (*Handler, error) { cone: ctx.Value("cone").(bool), } + a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) + if a.Encryption != "" && a.Encryption != "none" { + s := strings.Split(a.Encryption, ".") + var nfsPKeysBytes [][]byte + for _, r := range s { + b, _ := base64.RawURLEncoding.DecodeString(r) + nfsPKeysBytes = append(nfsPKeysBytes, b) + } + handler.encryption = &encryption.ClientInstance{} + if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds); err != nil { + return nil, errors.New("failed to use encryption").Base(err).AtError() + } + } + return handler, nil } @@ -98,6 +116,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte target := ob.Target errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination().NetAddr()) + if h.encryption != nil { + var err error + conn, err = h.encryption.Handshake(conn) + if err != nil { + return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo() + } + } + command := protocol.RequestCommandTCP if target.Network == net.Network_UDP { command = protocol.RequestCommandUDP @@ -120,6 +146,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte Flow: account.Flow, } + var peerCache *[]byte var input *bytes.Reader var rawInput *bytes.Buffer allowUDP443 := false @@ -138,6 +165,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte case protocol.RequestCommandMux: fallthrough // let server break Mux connections that contain TCP requests case protocol.RequestCommandTCP: + if clientConn, ok := conn.(*encryption.CommonConn); ok { + peerCache = &clientConn.PeerCache + if _, ok := clientConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransport(iConn) { + ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice + } + break + } var t reflect.Type var p uintptr if tlsConn, ok := iConn.(*tls.Conn); ok { @@ -272,7 +306,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } if requestAddons.Flow == vless.XRV { - err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, input, rawInput, trafficState, ob, false, ctx) + err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, peerCache, input, rawInput, trafficState, ob, false, ctx) } else { // from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBuffer err = buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer))