diff --git a/infra/conf/vless.go b/infra/conf/vless.go index d6f3727f..301e28bf 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -75,8 +75,8 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config.Decryption = c.Decryption if !func() bool { - s := strings.Split(config.Decryption, "-mlkem768seed-") - if len(s) != 2 { + s := strings.SplitN(config.Decryption, "-", 4) + if len(s) != 4 || s[2] != "mlkem768seed" { return false } if s[0] != "1rtt" { @@ -90,11 +90,18 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { } config.Minutes = uint32(i) } - b, err := base64.RawURLEncoding.DecodeString(s[1]) + switch s[1] { + case "vless": + case "aes128xor": + config.Xor = 1 + default: + return false + } + b, err := base64.RawURLEncoding.DecodeString(s[3]) if len(b) != 64 || err != nil { return false } - config.Decryption = s[1] + config.Decryption = s[3] return true }() && config.Decryption != "none" { if config.Decryption == "" { @@ -216,8 +223,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } if !func() bool { - s := strings.Split(account.Encryption, "-mlkem768client-") - if len(s) != 2 { + s := strings.SplitN(account.Encryption, "-", 4) + if len(s) != 4 || s[2] != "mlkem768client" { return false } if s[0] != "1rtt" { @@ -231,11 +238,18 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } account.Minutes = uint32(i) } - b, err := base64.RawURLEncoding.DecodeString(s[1]) + switch s[1] { + case "vless": + case "aes128xor": + account.Xor = 1 + default: + return false + } + b, err := base64.RawURLEncoding.DecodeString(s[3]) if len(b) != 1184 || err != nil { return false } - account.Encryption = s[1] + account.Encryption = s[3] return true }() && account.Encryption != "none" { if account.Encryption == "" { diff --git a/proxy/vless/account.go b/proxy/vless/account.go index 71b2f274..55c7b54c 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -18,6 +18,7 @@ func (a *Account) AsAccount() (protocol.Account, error) { ID: protocol.NewID(id), Flow: a.Flow, // needs parser here? Encryption: a.Encryption, // needs parser here? + Xor: a.Xor, Minutes: a.Minutes, }, nil } @@ -30,6 +31,7 @@ type MemoryAccount struct { Flow string Encryption string + Xor uint32 Minutes uint32 } @@ -47,6 +49,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Id: a.ID.String(), Flow: a.Flow, Encryption: a.Encryption, + Xor: a.Xor, Minutes: a.Minutes, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index be718d29..6cc66f04 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -30,7 +30,8 @@ type Account struct { // Flow settings. May be "xtls-rprx-vision". Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` - Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` + Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Account) Reset() { @@ -84,6 +85,13 @@ func (x *Account) GetEncryption() string { return "" } +func (x *Account) GetXor() uint32 { + if x != nil { + return x.Xor + } + return 0 +} + func (x *Account) GetMinutes() uint32 { if x != nil { return x.Minutes @@ -96,20 +104,21 @@ var File_proxy_vless_account_proto protoreflect.FileDescriptor var file_proxy_vless_account_proto_rawDesc = []byte{ 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x67, 0x0a, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x79, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, - 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, - 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, - 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, - 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, - 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, + 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, 0x6f, 0x72, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, + 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, + 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 1b82a836..199005ad 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -13,5 +13,6 @@ message Account { string flow = 2; string encryption = 3; - uint32 minutes = 4; + uint32 xor = 4; + uint32 minutes = 5; } diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 41240393..d253eaaa 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -28,6 +28,7 @@ func init() { type ClientInstance struct { sync.RWMutex eKeyNfs *mlkem.EncapsulationKey768 + xor uint32 minutes time.Duration expire time.Time baseKey []byte @@ -47,8 +48,9 @@ type ClientConn struct { peerCache []byte } -func (i *ClientInstance) Init(eKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ClientInstance) Init(eKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.eKeyNfs, err = mlkem.NewEncapsulationKey768(eKeyNfsData) + i.xor = xor i.minutes = minutes return } @@ -57,6 +59,9 @@ func (i *ClientInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.eKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.eKeyNfs.Bytes()) + } c := &ClientConn{Conn: conn} if i.minutes > 0 { diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index 4c365706..6507c8c0 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -26,6 +26,7 @@ type ServerSession struct { type ServerInstance struct { sync.RWMutex dKeyNfs *mlkem.DecapsulationKey768 + xor uint32 minutes time.Duration sessions map[[21]byte]*ServerSession } @@ -43,8 +44,9 @@ type ServerConn struct { nonce []byte } -func (i *ServerInstance) Init(dKeyNfsData []byte, minutes time.Duration) (err error) { +func (i *ServerInstance) Init(dKeyNfsData []byte, xor uint32, minutes time.Duration) (err error) { i.dKeyNfs, err = mlkem.NewDecapsulationKey768(dKeyNfsData) + i.xor = xor if minutes > 0 { i.minutes = minutes i.sessions = make(map[[21]byte]*ServerSession) @@ -69,6 +71,9 @@ func (i *ServerInstance) Handshake(conn net.Conn) (net.Conn, error) { if i.dKeyNfs == nil { return nil, errors.New("uninitialized") } + if i.xor == 1 { + conn = NewXorConn(conn, i.dKeyNfs.EncapsulationKey().Bytes()) + } c := &ServerConn{Conn: conn} peerTicketHello := make([]byte, 21+32) diff --git a/proxy/vless/encryption/xor.go b/proxy/vless/encryption/xor.go new file mode 100644 index 00000000..c8af2112 --- /dev/null +++ b/proxy/vless/encryption/xor.go @@ -0,0 +1,63 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +type XorConn struct { + net.Conn + key []byte + ctr cipher.Stream + peerCtr cipher.Stream +} + +func NewXorConn(conn net.Conn, key []byte) *XorConn { + return &XorConn{Conn: conn, key: key[:16]} +} + +func (c *XorConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + var iv []byte + if c.ctr == nil { + block, _ := aes.NewCipher(c.key) + iv = make([]byte, 16) + rand.Read(iv) + c.ctr = cipher.NewCTR(block, iv) + } + c.ctr.XORKeyStream(b, b) // caller MUST discard b + if iv != nil { + b = append(iv, b...) + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + if iv != nil { + b = b[16:] + } + return len(b), nil +} + +func (c *XorConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + if c.peerCtr == nil { + peerIv := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, peerIv); err != nil { + return 0, err + } + block, _ := aes.NewCipher(c.key) + c.peerCtr = cipher.NewCTR(block, peerIv) + } + n, err := c.Conn.Read(b) + if n > 0 { + c.peerCtr.XORKeyStream(b[:n], b[:n]) + } + return n, err +} diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go index 20837a4c..a125a7e4 100644 --- a/proxy/vless/inbound/config.pb.go +++ b/proxy/vless/inbound/config.pb.go @@ -114,7 +114,8 @@ type Config struct { Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` Decryption string `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"` - Minutes uint32 `protobuf:"varint,4,opt,name=minutes,proto3" json:"minutes,omitempty"` + Xor uint32 `protobuf:"varint,4,opt,name=xor,proto3" json:"xor,omitempty"` + Minutes uint32 `protobuf:"varint,5,opt,name=minutes,proto3" json:"minutes,omitempty"` } func (x *Config) Reset() { @@ -168,6 +169,13 @@ func (x *Config) GetDecryption() string { return "" } +func (x *Config) GetXor() uint32 { + if x != nil { + return x.Xor + } + return 0 +} + func (x *Config) GetMinutes() uint32 { if x != nil { return x.Minutes @@ -191,7 +199,7 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xba, 0x01, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xcc, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, @@ -202,15 +210,16 @@ var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1c, 0x63, 0x6f, - 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, - 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, - 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, - 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, - 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x49, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x10, 0x0a, 0x03, 0x78, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x78, + 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1c, + 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, + 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, + 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, + 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, + 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto index dc6bbac5..c96855a0 100644 --- a/proxy/vless/inbound/config.proto +++ b/proxy/vless/inbound/config.proto @@ -22,5 +22,6 @@ message Config { repeated Fallback fallbacks = 2; string decryption = 3; - uint32 minutes = 4; + uint32 xor = 4; + uint32 minutes = 5; } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 83af17e8..7c5e9005 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -87,7 +87,7 @@ func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Val d, _ := base64.RawURLEncoding.DecodeString(config.Decryption) if len(d) == 64 { handler.decryption = &encryption.ServerInstance{} - if err := handler.decryption.Init(d, time.Duration(config.Minutes)*time.Minute); err != nil { + if err := handler.decryption.Init(d, config.Xor, time.Duration(config.Minutes)*time.Minute); err != nil { return nil, errors.New("failed to use mlkem768seed").Base(err).AtError() } } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 5193e60a..1bbd879b 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -71,7 +71,7 @@ func New(ctx context.Context, config *Config) (*Handler, error) { e, _ := base64.RawURLEncoding.DecodeString(a.Encryption) if len(e) == 1184 { handler.encryption = &encryption.ClientInstance{} - if err := handler.encryption.Init(e, time.Duration(a.Minutes)*time.Minute); err != nil { + if err := handler.encryption.Init(e, a.Xor, time.Duration(a.Minutes)*time.Minute); err != nil { return nil, errors.New("failed to use mlkem768client").Base(err).AtError() } }