From 9b90719ddd8e596f983fd190729191e0f2419440 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 11 Aug 2025 20:57:23 +0800 Subject: [PATCH] feat: support optional aes128xor layer for vless encryption --- adapter/outbound/vless.go | 14 +++++-- docs/config.yaml | 6 ++- listener/inbound/vless_test.go | 25 +++++++---- listener/sing_vless/server.go | 14 +++++-- transport/vless/encryption/client.go | 7 +++- transport/vless/encryption/doc.go | 2 + transport/vless/encryption/server.go | 7 +++- transport/vless/encryption/xor.go | 63 ++++++++++++++++++++++++++++ 8 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 transport/vless/encryption/xor.go diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index a371b99a..de3c24ab 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -456,7 +456,7 @@ func NewVless(option VlessOption) (*Vless, error) { option: &option, } - if s := strings.Split(option.Encryption, "-mlkem768client-"); len(s) == 2 { + if s := strings.SplitN(option.Encryption, "-", 4); len(s) == 4 && s[2] == "mlkem768client" { var minutes uint32 if s[0] != "1rtt" { t := strings.TrimSuffix(s[0], "min") @@ -470,14 +470,22 @@ func NewVless(option VlessOption) (*Vless, error) { } minutes = uint32(i) } + var xor uint32 + switch s[1] { + case "vless": + case "aes128xor": + xor = 1 + default: + return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption) + } var b []byte - b, err = base64.RawURLEncoding.DecodeString(s[1]) + b, err = base64.RawURLEncoding.DecodeString(s[3]) if err != nil { return nil, fmt.Errorf("invaild vless encryption value: %s", option.Encryption) } if len(b) == encryption.MLKEM768ClientLength { v.encryption = &encryption.ClientInstance{} - if err = v.encryption.Init(b, time.Duration(minutes)*time.Minute); err != nil { + if err = v.encryption.Init(b, xor, time.Duration(minutes)*time.Minute); err != nil { return nil, fmt.Errorf("failed to use mlkem768seed: %w", err) } } else { diff --git a/docs/config.yaml b/docs/config.yaml index 67902946..245b38e8 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -638,7 +638,8 @@ proxies: # socks5 port: 443 uuid: uuid network: tcp - encryption: "8min-mlkem768client-bas64RawURLEncoding" # 复用八分钟后协商新的 sharedKey,需小于服务端的值 + encryption: "8min-vless-mlkem768client-bas64RawURLEncoding" # 复用八分钟后协商新的 sharedKey,需小于服务端的值 + # encryption: "8min-aes128xor-mlkem768client-bas64RawURLEncoding" tls: false #可以不开启tls udp: true @@ -1346,7 +1347,8 @@ listeners: flow: xtls-rprx-vision # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 - # decryption: "10min-mlkem768seed-bas64RawURLEncoding" # 同时允许 1-RTT 模式与十分钟复用的 0-RTT 模式, 后面base64字符串可由可由 mihomo generate vless-mlkem768 命令生成 + # decryption: "10min-vless-mlkem768seed-bas64RawURLEncoding" # 同时允许 1-RTT 模式与十分钟复用的 0-RTT 模式, 后面base64字符串可由可由 mihomo generate vless-mlkem768 命令生成 + # decryption: "10min-aes128xor-mlkem768seed-bas64RawURLEncoding" # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index a6e0d58b..2ef12126 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -94,13 +94,24 @@ func TestInboundVless_Encryption(t *testing.T) { t.Fatal(err) return } - inboundOptions := inbound.VlessOption{ - Decryption: "10min-mlkem768seed-" + seedBase64, - } - outboundOptions := outbound.VlessOption{ - Encryption: "8min-mlkem768client-" + clientBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) + t.Run("-vless-", func(t *testing.T) { + inboundOptions := inbound.VlessOption{ + Decryption: "10min-vless-mlkem768seed-" + seedBase64, + } + outboundOptions := outbound.VlessOption{ + Encryption: "8min-vless-mlkem768client-" + clientBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + }) + t.Run("-aes128xor-", func(t *testing.T) { + inboundOptions := inbound.VlessOption{ + Decryption: "10min-aes128xor-mlkem768seed-" + seedBase64, + } + outboundOptions := outbound.VlessOption{ + Encryption: "8min-aes128xor-mlkem768client-" + clientBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Wss1(t *testing.T) { diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index cdf453b5..41f23f34 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -88,7 +88,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{config: config, service: service} - if s := strings.Split(config.Decryption, "-mlkem768seed-"); len(s) == 2 { + if s := strings.SplitN(config.Decryption, "-", 4); len(s) == 4 && s[2] == "mlkem768seed" { var minutes uint32 if s[0] != "1rtt" { t := strings.TrimSuffix(s[0], "min") @@ -102,14 +102,22 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } minutes = uint32(i) } + var xor uint32 + switch s[1] { + case "vless": + case "aes128xor": + xor = 1 + default: + return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption) + } var b []byte - b, err = base64.RawURLEncoding.DecodeString(s[1]) + b, err = base64.RawURLEncoding.DecodeString(s[3]) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", config.Decryption) } if len(b) == encryption.MLKEM768SeedLength { sl.decryption = &encryption.ServerInstance{} - if err = sl.decryption.Init(b, time.Duration(minutes)*time.Minute); err != nil { + if err = sl.decryption.Init(b, xor, time.Duration(minutes)*time.Minute); err != nil { return nil, fmt.Errorf("failed to use mlkem768seed: %w", err) } } else { diff --git a/transport/vless/encryption/client.go b/transport/vless/encryption/client.go index 39d56731..8453ea61 100644 --- a/transport/vless/encryption/client.go +++ b/transport/vless/encryption/client.go @@ -38,6 +38,7 @@ func init() { type ClientInstance struct { sync.RWMutex eKeyNfs *mlkem.EncapsulationKey768 + xor uint32 minutes time.Duration expire time.Time baseKey []byte @@ -57,8 +58,9 @@ type ClientConn struct { peerCache []byte } -func (i *ClientInstance) Init(eKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ClientInstance) Init(eKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData) + i.xor = xor i.minutes = minutes return } @@ -67,6 +69,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.eKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.eKeyNfs.Bytes()) + } c := &ClientConn{Conn: conn} if i.minutes > 0 { diff --git a/transport/vless/encryption/doc.go b/transport/vless/encryption/doc.go index dbb00211..af531f8b 100644 --- a/transport/vless/encryption/doc.go +++ b/transport/vless/encryption/doc.go @@ -1,3 +1,5 @@ // Package encryption copy and modify from xray-core // https://github.com/XTLS/Xray-core/commit/f61c14e9c63dc41a8a09135db3aea337974f3f37 +// https://github.com/XTLS/Xray-core/commit/3e19bf9233bdd9bafc073a71c65b737cc1ffba5e +// https://github.com/XTLS/Xray-core/commit/7ffb555fc8ec51bd1e3e60f26f1d6957984dba80 package encryption diff --git a/transport/vless/encryption/server.go b/transport/vless/encryption/server.go index 04643ec6..68744459 100644 --- a/transport/vless/encryption/server.go +++ b/transport/vless/encryption/server.go @@ -25,6 +25,7 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex dKeyNfs *mlkem.DecapsulationKey768 + xor uint32 minutes time.Duration sessions map[[21]byte]*ServerSession stop bool @@ -43,8 +44,9 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(dKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData) + i.xor = xor if minutes > 0 { i.minutes = minutes i.sessions = make(map[[21]byte]*ServerSession) @@ -79,6 +81,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.dKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.dKeyNfs.EncapsulationKey().Bytes()) + } c := &ServerConn{Conn: conn} peerTicketHello := make([]byte, 21+32) diff --git a/transport/vless/encryption/xor.go b/transport/vless/encryption/xor.go new file mode 100644 index 00000000..c8af2112 --- /dev/null +++ b/transport/vless/encryption/xor.go @@ -0,0 +1,63 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +type XorConn struct { + net.Conn + key []byte + ctr cipher.Stream + peerCtr cipher.Stream +} + +func NewXorConn(conn net.Conn, key []byte) *XorConn { + return &XorConn{Conn: conn, key: key[:16]} +} + +func (c *XorConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + var iv []byte + if c.ctr == nil { + block, _ := aes.NewCipher(c.key) + iv = make([]byte, 16) + rand.Read(iv) + c.ctr = cipher.NewCTR(block, iv) + } + c.ctr.XORKeyStream(b, b) // caller MUST discard b + if iv != nil { + b = append(iv, b...) + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + if iv != nil { + b = b[16:] + } + return len(b), nil +} + +func (c *XorConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + if c.peerCtr == nil { + peerIv := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, peerIv); err != nil { + return 0, err + } + block, _ := aes.NewCipher(c.key) + c.peerCtr = cipher.NewCTR(block, peerIv) + } + n, err := c.Conn.Read(b) + if n > 0 { + c.peerCtr.XORKeyStream(b[:n], b[:n]) + } + return n, err +}