From 373558ed7abdbac3de41745cf30ec04c9adde604 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:17:35 +0000 Subject: [PATCH] Use X25519 for XOR; Add "divide" (ECH, before and includes type 0); Change config format https://github.com/XTLS/Xray-core/pull/4952#issuecomment-3207449672 --- infra/conf/vless.go | 40 ++++++++------ main/commands/all/curve25519.go | 41 +++++--------- main/commands/all/mlkem768.go | 9 ++- main/commands/all/x25519.go | 4 +- proxy/proxy.go | 16 +++++- proxy/vless/account.go | 6 +- proxy/vless/account.pb.go | 36 ++++++------ proxy/vless/account.proto | 2 +- proxy/vless/encryption/client.go | 33 ++++++----- proxy/vless/encryption/common.go | 2 +- proxy/vless/encryption/server.go | 36 +++++++----- proxy/vless/encryption/xor.go | 94 +++++++++++++++++++++++++------- proxy/vless/inbound/config.pb.go | 29 +++++----- proxy/vless/inbound/config.proto | 2 +- proxy/vless/inbound/inbound.go | 17 ++---- proxy/vless/outbound/outbound.go | 17 +++--- 16 files changed, 225 insertions(+), 159 deletions(-) diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 5fcdf34c..44503912 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -71,8 +71,8 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Decryption = c.Decryption if !func() bool { - s := strings.SplitN(config.Decryption, "-", 4) - if len(s) != 4 || s[2] != "mlkem768seed" { + s := strings.Split(config.Decryption, ".") + if len(s) != 5 || s[2] != "mlkem768Seed" { return false } if s[0] != "1rtt" { @@ -87,17 +87,21 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Minutes = uint32(i) } switch s[1] { - case "vless": - case "xored": - config.Xor = 1 + case "native": + case "divide": + config.XorMode = 1 + case "random": + config.XorMode = 2 default: return false } - b, err := base64.RawURLEncoding.DecodeString(s[3]) - if len(b) != 64 || err != nil { + if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { return false } - config.Decryption = s[3] + if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 64 { + return false + } + config.Decryption = s[4] + "." + s[3] return true }() && config.Decryption != "none" { if config.Decryption == "" { @@ -215,8 +219,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } if !func() bool { - s := strings.SplitN(account.Encryption, "-", 4) - if len(s) != 4 || s[2] != "mlkem768client" { + s := strings.Split(account.Encryption, ".") + if len(s) != 5 || s[2] != "mlkem768Client" { return false } if s[0] != "1rtt" { @@ -231,17 +235,21 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { account.Minutes = uint32(i) } switch s[1] { - case "vless": - case "xored": - account.Xor = 1 + case "native": + case "divide": + account.XorMode = 1 + case "random": + account.XorMode = 2 default: return false } - b, err := base64.RawURLEncoding.DecodeString(s[3]) - if len(b) != 1184 || err != nil { + if b, _ := base64.RawURLEncoding.DecodeString(s[3]); len(b) != 32 { return false } - account.Encryption = s[3] + if b, _ := base64.RawURLEncoding.DecodeString(s[4]); len(b) != 1184 { + return false + } + account.Encryption = s[4] + "." + s[3] return true }() && account.Encryption != "none" { if account.Encryption == "" { diff --git a/main/commands/all/curve25519.go b/main/commands/all/curve25519.go index bb706c6c..ddaebdfd 100644 --- a/main/commands/all/curve25519.go +++ b/main/commands/all/curve25519.go @@ -1,17 +1,13 @@ package all import ( + "crypto/ecdh" "crypto/rand" "encoding/base64" "fmt" - - "golang.org/x/crypto/curve25519" ) func Curve25519Genkey(StdEncoding bool, input_base64 string) { - var output string - var err error - var privateKey, publicKey []byte var encoding *base64.Encoding if *input_stdEncoding || StdEncoding { encoding = base64.StdEncoding @@ -19,24 +15,17 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { encoding = base64.RawURLEncoding } + var privateKey []byte if len(input_base64) > 0 { - privateKey, err = encoding.DecodeString(input_base64) - if err != nil { - output = err.Error() - goto out - } - if len(privateKey) != curve25519.ScalarSize { - output = "Invalid length of private key." - goto out + privateKey, _ = encoding.DecodeString(input_base64) + if len(privateKey) != 32 { + fmt.Println("Invalid length of X25519 private key.") + return } } - if privateKey == nil { - privateKey = make([]byte, curve25519.ScalarSize) - if _, err = rand.Read(privateKey); err != nil { - output = err.Error() - goto out - } + privateKey = make([]byte, 32) + rand.Read(privateKey) } // Modify random bytes using algorithm described at: @@ -45,14 +34,12 @@ func Curve25519Genkey(StdEncoding bool, input_base64 string) { privateKey[31] &= 127 privateKey[31] |= 64 - if publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint); err != nil { - output = err.Error() - goto out + key, err := ecdh.X25519().NewPrivateKey(privateKey) + if err != nil { + fmt.Println(err.Error()) + return } - - output = fmt.Sprintf("Private key: %v\nPublic key: %v", + fmt.Printf("PrivateKey: %v\nPassword: %v", encoding.EncodeToString(privateKey), - encoding.EncodeToString(publicKey)) -out: - fmt.Println(output) + encoding.EncodeToString(key.PublicKey().Bytes())) } diff --git a/main/commands/all/mlkem768.go b/main/commands/all/mlkem768.go index 78512bad..f3cb0f79 100644 --- a/main/commands/all/mlkem768.go +++ b/main/commands/all/mlkem768.go @@ -3,6 +3,7 @@ package all import ( "crypto/mlkem" "crypto/rand" + "crypto/sha3" "encoding/base64" "fmt" @@ -40,8 +41,10 @@ func executeMLKEM768(cmd *base.Command, args []string) { rand.Read(seed[:]) } key, _ := mlkem.NewDecapsulationKey768(seed[:]) - pub := key.EncapsulationKey() - fmt.Printf("Seed: %v\nClient: %v", + client := key.EncapsulationKey().Bytes() + hash32 := sha3.Sum256(client) + fmt.Printf("Seed: %v\nClient: %v\nHash11: %v", base64.RawURLEncoding.EncodeToString(seed[:]), - base64.RawURLEncoding.EncodeToString(pub.Bytes())) + base64.RawURLEncoding.EncodeToString(client), + base64.RawURLEncoding.EncodeToString(hash32[:11])) } diff --git a/main/commands/all/x25519.go b/main/commands/all/x25519.go index 607562b6..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 (REALITY)`, + Short: `Generate key pair for X25519 key exchange (VLESS, REALITY)`, Long: ` -Generate key pair for X25519 key exchange (REALITY). +Generate key pair for X25519 key exchange (VLESS, REALITY). Random: {{.Exec}} x25519 diff --git a/proxy/proxy.go b/proxy/proxy.go index 188cf4e9..8251849d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -539,7 +539,10 @@ func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { isEncryption = true } if xorConn, ok := conn.(*encryption.XorConn); ok { - return xorConn, nil, nil // xorConn should not be penetrated + if !xorConn.Divide { + return xorConn, nil, nil // full-random xorConn should not be penetrated + } + conn = xorConn.Conn } if statConn, ok := conn.(*stat.CounterConnection); ok { conn = statConn.Connection @@ -652,3 +655,14 @@ func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer sign } return nil } + +func IsRAWTransport(conn stat.Connection) bool { + iConn := conn + if statConn, ok := iConn.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + _, ok1 := iConn.(*proxyproto.Conn) + _, ok2 := iConn.(*net.TCPConn) + _, ok3 := iConn.(*internet.UnixConnWrapper) + return ok1 || ok2 || ok3 +} diff --git a/proxy/vless/account.go b/proxy/vless/account.go index 55c7b54c..9967c7e1 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,7 +18,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? - Xor: a.Xor, + XorMode: a.XorMode, Minutes: a.Minutes, }, nil } @@ -31,7 +31,7 @@ type MemoryAccount struct { Flow string Encryption string - Xor uint32 + XorMode uint32 Minutes uint32 } @@ -49,7 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, - Xor: a.Xor, + XorMode: a.XorMode, Minutes: a.Minutes, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index 6cc66f04..ca638de1 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -30,7 +30,7 @@ type Account struct { // Flow settings. May be "xtls-rprx-vision". Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` - Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } @@ -85,9 +85,9 @@ func (x *Account) GetEncryption() string { return "" } -func (x *Account) GetXor() uint32 { +func (x *Account) GetXorMode() uint32 { if x != nil { - return x.Xor + return x.XorMode } return 0 } @@ -104,21 +104,21 @@ var File_proxy_vless_account_proto protoreflect.FileDescriptor var file_proxy_vless_account_proto_rawDesc = []byte{ 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x79, 0x0a, - 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, 0x0a, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, - 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, 0x6f, 0x72, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, - 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, - 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, - 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x81, 0x01, + 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, + 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, + 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, + 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, + 0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, + 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, + 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 199005ad..f91a2528 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -13,6 +13,6 @@ message Account { string flow = 2; string encryption = 3; - uint32 xor = 4; + uint32 xorMode = 4; uint32 minutes = 5; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 101d2052..7eff2eb8 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -3,6 +3,7 @@ package encryption import ( "bytes" "crypto/cipher" + "crypto/ecdh" "crypto/mlkem" "crypto/rand" "crypto/sha3" @@ -29,7 +30,8 @@ type ClientInstance struct { sync.RWMutex nfsEKey *mlkem.EncapsulationKey768 hash11 [11]byte // no more capacity - xorKey []byte + xorMode uint32 + xorPKey *ecdh.PublicKey minutes time.Duration expire time.Time baseKey []byte @@ -49,22 +51,23 @@ type ClientConn struct { PeerCache []byte } -func (i *ClientInstance) Init(nfsEKeyBytes []byte, xor uint32, minutes time.Duration) (err error) { +func (i *ClientInstance) Init(nfsEKeyBytes, xorPKeyBytes []byte, xorMode, minutes uint32) (err error) { if i.nfsEKey != nil { err = errors.New("already initialized") return } - i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes) - if err != nil { + if i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes); err != nil { return } hash32 := sha3.Sum256(nfsEKeyBytes) copy(i.hash11[:], hash32[:]) - if xor > 0 { - xorKey := sha3.Sum256(nfsEKeyBytes) - i.xorKey = xorKey[:] + if xorMode > 0 { + i.xorMode = xorMode + if i.xorPKey, err = ecdh.X25519().NewPublicKey(xorPKeyBytes); err != nil { + return + } } - i.minutes = minutes + i.minutes = time.Duration(minutes) * time.Minute return } @@ -72,8 +75,8 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { if i.nfsEKey == nil { return nil, errors.New("uninitialized") } - if i.xorKey != nil { - conn = NewXorConn(conn, i.xorKey) + if i.xorMode > 0 { + conn, _ = NewXorConn(conn, i.xorMode, i.xorPKey, nil) } c := &ClientConn{Conn: conn} @@ -134,7 +137,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) { } c.baseKey = append(pfsKey, nfsKey...) - VLESS, _ := NewAead(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) + VLESS, _ := NewAEAD(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes) if !bytes.Equal(VLESS, []byte("VLESS")) { return nil, errors.New("invalid server").AtError() } @@ -169,7 +172,7 @@ func (c *ClientConn) Write(b []byte) (int, error) { rand.Read(c.random) copy(data[5+32:], c.random) EncodeHeader(data[5+32+32:], 23, len(b)+16) - c.aead = NewAead(ClientCipher, c.baseKey, c.random, c.ticket) + c.aead = NewAEAD(ClientCipher, c.baseKey, c.random, c.ticket) c.nonce = make([]byte, 12) c.aead.Seal(data[:5+32+32+5], c.nonce, b, data[5+32+32:5+32+32+5]) } else { @@ -177,7 +180,7 @@ func (c *ClientConn) Write(b []byte) (int, error) { EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAead(ClientCipher, c.baseKey, data[5:], data[:5]) + c.aead = NewAEAD(ClientCipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) @@ -218,7 +221,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { if c.random == nil { return 0, errors.New("empty c.random") } - c.peerAead = NewAead(ClientCipher, c.baseKey, peerRandomHello, c.random) + c.peerAead = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -243,7 +246,7 @@ func (c *ClientConn) Read(b []byte) (int, error) { } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(ClientCipher, c.baseKey, peerData, h) + peerAead = NewAEAD(ClientCipher, c.baseKey, peerData, h) } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { diff --git a/proxy/vless/encryption/common.go b/proxy/vless/encryption/common.go index d4484bd6..6de517e0 100644 --- a/proxy/vless/encryption/common.go +++ b/proxy/vless/encryption/common.go @@ -72,7 +72,7 @@ func ReadAndDiscardPaddings(conn net.Conn) (h []byte, t byte, l int, err error) } } -func NewAead(c byte, secret, salt, info []byte) (aead cipher.AEAD) { +func NewAEAD(c byte, secret, salt, info []byte) (aead cipher.AEAD) { key, _ := hkdf.Key(sha3.New256, secret, salt, string(info), 32) if c&1 == 1 { block, _ := aes.NewCipher(key) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index a9ade3d4..17c8c689 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -3,6 +3,7 @@ package encryption import ( "bytes" "crypto/cipher" + "crypto/ecdh" "crypto/mlkem" "crypto/rand" "crypto/sha3" @@ -27,7 +28,8 @@ type ServerInstance struct { sync.RWMutex nfsDKey *mlkem.DecapsulationKey768 hash11 [11]byte // no more capacity - xorKey []byte + xorMode uint32 + xorSKey *ecdh.PrivateKey minutes time.Duration sessions map[[32]byte]*ServerSession closed bool @@ -46,23 +48,24 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(nfsDKeySeed []byte, xor uint32, minutes time.Duration) (err error) { +func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes uint32) (err error) { if i.nfsDKey != nil { err = errors.New("already initialized") return } - i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed) - if err != nil { + if i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed); err != nil { return } hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) copy(i.hash11[:], hash32[:]) - if xor > 0 { - xorKey := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes()) - i.xorKey = xorKey[:] + if xorMode > 0 { + i.xorMode = xorMode + if i.xorSKey, err = ecdh.X25519().NewPrivateKey(xorSKeyBytes); err != nil { + return + } } if minutes > 0 { - i.minutes = minutes + i.minutes = time.Duration(minutes) * time.Minute i.sessions = make(map[[32]byte]*ServerSession) go func() { for { @@ -96,8 +99,11 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { if i.nfsDKey == nil { return nil, errors.New("uninitialized") } - if i.xorKey != nil { - conn = NewXorConn(conn, i.xorKey) + if i.xorMode > 0 { + var err error + if conn, err = NewXorConn(conn, i.xorMode, nil, i.xorSKey); err != nil { + return nil, err + } } c := &ServerConn{Conn: conn} @@ -168,7 +174,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) { pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate() c.baseKey = append(pfsKey, nfsKey...) - c.ticket = append(i.hash11[:], NewAead(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) + c.ticket = append(i.hash11[:], NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Seal(nil, peerClientHello[:12], []byte("VLESS"), pfsEKeyBytes)...) paddingLen := crypto.RandBetween(100, 1000) @@ -222,7 +228,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } c.peerRandom = peerTicketHello[32:] } - c.peerAead = NewAead(c.cipher, c.baseKey, c.peerRandom, c.ticket) + c.peerAead = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket) c.peerNonce = make([]byte, 12) } if len(c.PeerCache) != 0 { @@ -247,7 +253,7 @@ func (c *ServerConn) Read(b []byte) (int, error) { } var peerAead cipher.AEAD if bytes.Equal(c.peerNonce, MaxNonce) { - peerAead = NewAead(c.cipher, c.baseKey, peerData, h) + peerAead = NewAEAD(c.cipher, c.baseKey, peerData, h) } _, err = c.peerAead.Open(dst[:0], c.peerNonce, peerData, h) if peerAead != nil { @@ -283,7 +289,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { EncodeHeader(data, 0, 32) rand.Read(data[5 : 5+32]) EncodeHeader(data[5+32:], 23, len(b)+16) - c.aead = NewAead(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) + c.aead = NewAEAD(c.cipher, c.baseKey, data[5:5+32], c.peerRandom) c.nonce = make([]byte, 12) c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5]) } else { @@ -291,7 +297,7 @@ func (c *ServerConn) Write(b []byte) (int, error) { EncodeHeader(data, 23, len(b)+16) c.aead.Seal(data[:5], c.nonce, b, data[:5]) if bytes.Equal(c.nonce, MaxNonce) { - c.aead = NewAead(c.cipher, c.baseKey, data[5:], data[:5]) + c.aead = NewAEAD(c.cipher, c.baseKey, data[5:], data[:5]) } } IncreaseNonce(c.nonce) diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go index caad12bf..bac45e46 100644 --- a/proxy/vless/encryption/xor.go +++ b/proxy/vless/encryption/xor.go @@ -3,13 +3,21 @@ package encryption import ( "crypto/aes" "crypto/cipher" + "crypto/ecdh" + "crypto/hkdf" "crypto/rand" + "crypto/sha3" "io" "net" + + "github.com/xtls/xray-core/common/errors" ) type XorConn struct { net.Conn + Divide bool + + head []byte key []byte ctr cipher.Stream peerCtr cipher.Stream @@ -25,8 +33,55 @@ type XorConn struct { in_skip int } -func NewXorConn(conn net.Conn, key []byte) *XorConn { - return &XorConn{Conn: conn, key: key} +func NewCTR(key, iv []byte, isServer bool) cipher.Stream { + info := "CLIENT" + if isServer { + info = "SERVER" // avoids attackers sending traffic back to the client, though the encryption layer has its own protection + } + key, _ = hkdf.Key(sha3.New256, key, iv, info, 32) // avoids using pKey directly if attackers sent the basepoint, or whaterver they like + block, _ := aes.NewCipher(key) + return cipher.NewCTR(block, iv) +} + +func NewXorConn(conn net.Conn, mode uint32, pKey *ecdh.PublicKey, sKey *ecdh.PrivateKey) (*XorConn, error) { + if mode == 0 || (pKey == nil && sKey == nil) || (pKey != nil && sKey != nil) { + return nil, errors.New("invalid parameters") + } + c := &XorConn{ + Conn: conn, + Divide: mode == 1, + isHeader: true, + out_header: make([]byte, 0, 5), // important + in_header: make([]byte, 0, 5), // important + } + if pKey != nil { + c.head = make([]byte, 16+32) + rand.Read(c.head) + eSKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + NewCTR(pKey.Bytes(), c.head[:16], false).XORKeyStream(c.head[16:], eSKey.PublicKey().Bytes()) // make X25519 public key distinguishable from random bytes + c.key, _ = eSKey.ECDH(pKey) + c.ctr = NewCTR(c.key, c.head[:16], false) + } + if sKey != nil { + peerHead := make([]byte, 16+32) + if _, err := io.ReadFull(c.Conn, peerHead); err != nil { + return nil, err + } + NewCTR(sKey.PublicKey().Bytes(), peerHead[:16], false).XORKeyStream(peerHead[16:], peerHead[16:]) // we don't use buggy elligator, because we have PSK :) + ePKey, err := ecdh.X25519().NewPublicKey(peerHead[16:]) + if err != nil { + return nil, err + } + key, err := sKey.ECDH(ePKey) + if err != nil { + return nil, err + } + c.peerCtr = NewCTR(key, peerHead[:16], false) + c.head = make([]byte, 16) + rand.Read(c.head) // make sure the server always replies random bytes even when received replays, though it is not important + c.ctr = NewCTR(key, c.head, true) // the same key links the upload & download, though the encryption layer has its own link + } + return c, nil //chacha20.NewUnauthenticatedCipher() } @@ -35,13 +90,6 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records return 0, nil } if !c.out_after0 { - var iv []byte - if c.ctr == nil { - block, _ := aes.NewCipher(c.key) - iv = make([]byte, 16) - rand.Read(iv) - c.ctr = cipher.NewCTR(block, iv) - } t, l, _ := DecodeHeader(b) if t == 23 { // single 23 l = 5 @@ -49,20 +97,24 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records l += 10 if t == 0 { c.out_after0 = true - c.out_header = make([]byte, 0, 5) // important + if c.Divide { + l -= 5 + } } } c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b - if iv != nil { - b = append(iv, b...) + l = len(b) + if c.head != nil { + b = append(c.head, b...) + c.head = nil } if _, err := c.Conn.Write(b); err != nil { return 0, err } - if iv != nil { - b = b[16:] // for len(b) - } - return len(b), nil + return l, nil + } + if c.Divide { + return c.Conn.Write(b) } for p := b; ; { // for XTLS if len(p) <= c.out_skip { @@ -93,14 +145,12 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... return 0, nil } if !c.in_after0 || !c.isHeader { - if c.peerCtr == nil { + if c.peerCtr == nil { // for client peerIv := make([]byte, 16) if _, err := io.ReadFull(c.Conn, peerIv); err != nil { return 0, err } - block, _ := aes.NewCipher(c.key) - c.peerCtr = cipher.NewCTR(block, peerIv) - c.isHeader = true + c.peerCtr = NewCTR(c.key, peerIv, true) } if _, err := io.ReadFull(c.Conn, b); err != nil { return 0, err @@ -117,7 +167,6 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... c.isHeader = false if t == 0 { c.in_after0 = true - c.in_header = make([]byte, 0, 5) // important } } } else { @@ -125,6 +174,9 @@ func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes... } return len(b), nil } + if c.Divide { + return c.Conn.Read(b) + } n, err := c.Conn.Read(b) for p := b[:n]; ; { // for XTLS if len(p) <= c.in_skip { diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index a125a7e4..240c25d9 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -114,7 +114,7 @@ type Config struct { Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` Decryption string `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"` - Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } @@ -169,9 +169,9 @@ func (x *Config) GetDecryption() string { return "" } -func (x *Config) GetXor() uint32 { +func (x *Config) GetXorMode() uint32 { if x != nil { - return x.Xor + return x.XorMode } return 0 } @@ -199,7 +199,7 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xcc, 0x01, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xd4, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, @@ -210,16 +210,17 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x10, 0x0a, 0x03, 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, - 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1c, - 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, - 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, - 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, - 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, - 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, - 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x18, 0x0a, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x78, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, + 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, + 0x75, 0x74, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto index c96855a0..186d8588 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -22,6 +22,6 @@ message Config { repeated Fallback fallbacks = 2; string decryption = 3; - uint32 xor = 4; + uint32 xorMode = 4; uint32 minutes = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 39a192b8..fc8dd243 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -12,7 +12,6 @@ import ( "time" "unsafe" - "github.com/pires/go-proxyproto" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -32,7 +31,6 @@ import ( "github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless/encoding" "github.com/xtls/xray-core/proxy/vless/encryption" - "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -86,10 +84,11 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val validator: validator, } - d, _ := base64.RawURLEncoding.DecodeString(config.Decryption) - if len(d) == 64 { + if s := strings.Split(config.Decryption, "."); len(s) == 2 { + nfsDKeySeed, _ := base64.RawURLEncoding.DecodeString(s[0]) + xorSKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) handler.decryption = &encryption.ServerInstance{} - if err := handler.decryption.Init(d, config.Xor, time.Duration(config.Minutes)*time.Minute); err != nil { + if err := handler.decryption.Init(nfsDKeySeed, xorSKeyBytes, config.XorMode, config.Minutes); err != nil { return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() } } @@ -501,12 +500,8 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s case protocol.RequestCommandTCP: if serverConn, ok := connection.(*encryption.ServerConn); ok { peerCache = &serverConn.PeerCache - _, ok0 := serverConn.Conn.(*encryption.XorConn) - _, ok1 := iConn.(*proxyproto.Conn) - _, ok2 := iConn.(*net.TCPConn) - _, ok3 := iConn.(*internet.UnixConnWrapper) - if ok0 || (!ok1 && !ok2 && !ok3) { - inbound.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + if xorConn, ok := serverConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice } break } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 42b2a608..51974825 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -6,10 +6,10 @@ import ( gotls "crypto/tls" "encoding/base64" "reflect" + "strings" "time" "unsafe" - "github.com/pires/go-proxyproto" utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -69,10 +69,11 @@ func New(ctx context.Context, config *Config) (*Handler, error) { } a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) - e, _ := base64.RawURLEncoding.DecodeString(a.Encryption) - if len(e) == 1184 { + if s := strings.Split(a.Encryption, "."); len(s) == 2 { + nfsEKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[0]) + xorPKeyBytes, _ := base64.RawURLEncoding.DecodeString(s[1]) handler.encryption = &encryption.ClientInstance{} - if err := handler.encryption.Init(e, a.Xor, time.Duration(a.Minutes)*time.Minute); err != nil { + if err := handler.encryption.Init(nfsEKeyBytes, xorPKeyBytes, a.XorMode, a.Minutes); err != nil { return nil, errors.New("failed to use mlkem768client").Base(err).AtError() } } @@ -162,12 +163,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte case protocol.RequestCommandTCP: if clientConn, ok := conn.(*encryption.ClientConn); ok { peerCache = &clientConn.PeerCache - _, ok0 := clientConn.Conn.(*encryption.XorConn) - _, ok1 := iConn.(*proxyproto.Conn) - _, ok2 := iConn.(*net.TCPConn) - _, ok3 := iConn.(*internet.UnixConnWrapper) - if ok0 || (!ok1 && !ok2 && !ok3) { - ob.CanSpliceCopy = 3 // xorConn/non-RAW can not use Linux Splice + if xorConn, ok := clientConn.Conn.(*encryption.XorConn); (ok && !xorConn.Divide) || !proxy.IsRAWTransport(iConn) { + ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport can not use Linux Splice } break }