mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-04-30 09:18:34 +00:00
v1.0.0
This commit is contained in:
parent
47d23e9972
commit
c7f7c08ead
711 changed files with 82154 additions and 2 deletions
51
proxy/vmess/account.go
Normal file
51
proxy/vmess/account.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// +build !confonly
|
||||
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
)
|
||||
|
||||
// MemoryAccount is an in-memory form of VMess account.
|
||||
type MemoryAccount struct {
|
||||
// ID is the main ID of the account.
|
||||
ID *protocol.ID
|
||||
// AlterIDs are the alternative IDs of the account.
|
||||
AlterIDs []*protocol.ID
|
||||
// Security type of the account. Used for client connections.
|
||||
Security protocol.SecurityType
|
||||
}
|
||||
|
||||
// AnyValidID returns an ID that is either the main ID or one of the alternative IDs if any.
|
||||
func (a *MemoryAccount) AnyValidID() *protocol.ID {
|
||||
if len(a.AlterIDs) == 0 {
|
||||
return a.ID
|
||||
}
|
||||
return a.AlterIDs[dice.Roll(len(a.AlterIDs))]
|
||||
}
|
||||
|
||||
// Equals implements protocol.Account.
|
||||
func (a *MemoryAccount) Equals(account protocol.Account) bool {
|
||||
vmessAccount, ok := account.(*MemoryAccount)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO: handle AlterIds difference
|
||||
return a.ID.Equals(vmessAccount.ID)
|
||||
}
|
||||
|
||||
// AsAccount implements protocol.Account.
|
||||
func (a *Account) AsAccount() (protocol.Account, error) {
|
||||
id, err := uuid.ParseString(a.Id)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse ID").Base(err).AtError()
|
||||
}
|
||||
protoID := protocol.NewID(id)
|
||||
return &MemoryAccount{
|
||||
ID: protoID,
|
||||
AlterIDs: protocol.NewAlterIDs(protoID, uint16(a.AlterId)),
|
||||
Security: a.SecuritySettings.GetSecurityType(),
|
||||
}, nil
|
||||
}
|
195
proxy/vmess/account.pb.go
Normal file
195
proxy/vmess/account.pb.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.14.0
|
||||
// source: proxy/vmess/account.proto
|
||||
|
||||
package vmess
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
protocol "github.com/xtls/xray-core/v1/common/protocol"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type Account struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// 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"`
|
||||
// Number of alternative IDs. Client and server must share the same number.
|
||||
AlterId uint32 `protobuf:"varint,2,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
// Security settings. Only applies to client side.
|
||||
SecuritySettings *protocol.SecurityConfig `protobuf:"bytes,3,opt,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"`
|
||||
// Define tests enabled for this account
|
||||
TestsEnabled string `protobuf:"bytes,4,opt,name=tests_enabled,json=testsEnabled,proto3" json:"tests_enabled,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Account) Reset() {
|
||||
*x = Account{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proxy_vmess_account_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Account) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Account) ProtoMessage() {}
|
||||
|
||||
func (x *Account) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_vmess_account_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Account.ProtoReflect.Descriptor instead.
|
||||
func (*Account) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_vmess_account_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Account) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Account) GetAlterId() uint32 {
|
||||
if x != nil {
|
||||
return x.AlterId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Account) GetSecuritySettings() *protocol.SecurityConfig {
|
||||
if x != nil {
|
||||
return x.SecuritySettings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Account) GetTestsEnabled() string {
|
||||
if x != nil {
|
||||
return x.TestsEnabled
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_vmess_account_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proxy_vmess_account_proto_rawDesc = []byte{
|
||||
0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 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, 0x6d, 0x65, 0x73, 0x73, 0x1a, 0x1d, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x68,
|
||||
0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac, 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, 0x19, 0x0a, 0x08, 0x61, 0x6c, 0x74, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x6c, 0x74, 0x65,
|
||||
0x72, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x11, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f,
|
||||
0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24,
|
||||
0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x53, 0x65,
|
||||
0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x73, 0x74, 0x73, 0x5f,
|
||||
0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74,
|
||||
0x65, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x55, 0x0a, 0x14, 0x63,
|
||||
0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d,
|
||||
0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x28, 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, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0xaa,
|
||||
0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6d, 0x65,
|
||||
0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_proxy_vmess_account_proto_rawDescOnce sync.Once
|
||||
file_proxy_vmess_account_proto_rawDescData = file_proxy_vmess_account_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_proxy_vmess_account_proto_rawDescGZIP() []byte {
|
||||
file_proxy_vmess_account_proto_rawDescOnce.Do(func() {
|
||||
file_proxy_vmess_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_account_proto_rawDescData)
|
||||
})
|
||||
return file_proxy_vmess_account_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_vmess_account_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_proxy_vmess_account_proto_goTypes = []interface{}{
|
||||
(*Account)(nil), // 0: xray.proxy.vmess.Account
|
||||
(*protocol.SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig
|
||||
}
|
||||
var file_proxy_vmess_account_proto_depIdxs = []int32{
|
||||
1, // 0: xray.proxy.vmess.Account.security_settings:type_name -> xray.common.protocol.SecurityConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_vmess_account_proto_init() }
|
||||
func file_proxy_vmess_account_proto_init() {
|
||||
if File_proxy_vmess_account_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_proxy_vmess_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Account); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proxy_vmess_account_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_vmess_account_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_vmess_account_proto_depIdxs,
|
||||
MessageInfos: file_proxy_vmess_account_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_vmess_account_proto = out.File
|
||||
file_proxy_vmess_account_proto_rawDesc = nil
|
||||
file_proxy_vmess_account_proto_goTypes = nil
|
||||
file_proxy_vmess_account_proto_depIdxs = nil
|
||||
}
|
21
proxy/vmess/account.proto
Normal file
21
proxy/vmess/account.proto
Normal file
|
@ -0,0 +1,21 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package xray.proxy.vmess;
|
||||
option csharp_namespace = "Xray.Proxy.Vmess";
|
||||
option go_package = "github.com/xtls/xray-core/v1/proxy/vmess";
|
||||
option java_package = "com.xray.proxy.vmess";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protocol/headers.proto";
|
||||
|
||||
message Account {
|
||||
// ID of the account, in the form of a UUID, e.g.,
|
||||
// "66ad4540-b58c-4ad2-9926-ea63445a9b57".
|
||||
string id = 1;
|
||||
// Number of alternative IDs. Client and server must share the same number.
|
||||
uint32 alter_id = 2;
|
||||
// Security settings. Only applies to client side.
|
||||
xray.common.protocol.SecurityConfig security_settings = 3;
|
||||
// Define tests enabled for this account
|
||||
string tests_enabled = 4;
|
||||
}
|
119
proxy/vmess/aead/authid.go
Normal file
119
proxy/vmess/aead/authid.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package aead
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
rand3 "crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/antireplay"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("user do not exist")
|
||||
ErrReplay = errors.New("replayed request")
|
||||
)
|
||||
|
||||
func CreateAuthID(cmdKey []byte, time int64) [16]byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
common.Must(binary.Write(buf, binary.BigEndian, time))
|
||||
var zero uint32
|
||||
common.Must2(io.CopyN(buf, rand3.Reader, 4))
|
||||
zero = crc32.ChecksumIEEE(buf.Bytes())
|
||||
common.Must(binary.Write(buf, binary.BigEndian, zero))
|
||||
aesBlock := NewCipherFromKey(cmdKey)
|
||||
if buf.Len() != 16 {
|
||||
panic("Size unexpected")
|
||||
}
|
||||
var result [16]byte
|
||||
aesBlock.Encrypt(result[:], buf.Bytes())
|
||||
return result
|
||||
}
|
||||
|
||||
func NewCipherFromKey(cmdKey []byte) cipher.Block {
|
||||
aesBlock, err := aes.NewCipher(KDF16(cmdKey, KDFSaltConstAuthIDEncryptionKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return aesBlock
|
||||
}
|
||||
|
||||
type AuthIDDecoder struct {
|
||||
s cipher.Block
|
||||
}
|
||||
|
||||
func NewAuthIDDecoder(cmdKey []byte) *AuthIDDecoder {
|
||||
return &AuthIDDecoder{NewCipherFromKey(cmdKey)}
|
||||
}
|
||||
|
||||
func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) {
|
||||
aidd.s.Decrypt(data[:], data[:])
|
||||
var t int64
|
||||
var zero uint32
|
||||
var rand int32
|
||||
reader := bytes.NewReader(data[:])
|
||||
common.Must(binary.Read(reader, binary.BigEndian, &t))
|
||||
common.Must(binary.Read(reader, binary.BigEndian, &rand))
|
||||
common.Must(binary.Read(reader, binary.BigEndian, &zero))
|
||||
return t, zero, rand, data[:]
|
||||
}
|
||||
|
||||
func NewAuthIDDecoderHolder() *AuthIDDecoderHolder {
|
||||
return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewReplayFilter(120)}
|
||||
}
|
||||
|
||||
type AuthIDDecoderHolder struct {
|
||||
decoders map[string]*AuthIDDecoderItem
|
||||
filter *antireplay.ReplayFilter
|
||||
}
|
||||
|
||||
type AuthIDDecoderItem struct {
|
||||
dec *AuthIDDecoder
|
||||
ticket interface{}
|
||||
}
|
||||
|
||||
func NewAuthIDDecoderItem(key [16]byte, ticket interface{}) *AuthIDDecoderItem {
|
||||
return &AuthIDDecoderItem{
|
||||
dec: NewAuthIDDecoder(key[:]),
|
||||
ticket: ticket,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthIDDecoderHolder) AddUser(key [16]byte, ticket interface{}) {
|
||||
a.decoders[string(key[:])] = NewAuthIDDecoderItem(key, ticket)
|
||||
}
|
||||
|
||||
func (a *AuthIDDecoderHolder) RemoveUser(key [16]byte) {
|
||||
delete(a.decoders, string(key[:]))
|
||||
}
|
||||
|
||||
func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) {
|
||||
for _, v := range a.decoders {
|
||||
t, z, _, d := v.dec.Decode(authID)
|
||||
if z != crc32.ChecksumIEEE(d[:12]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if t < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if math.Abs(math.Abs(float64(t))-float64(time.Now().Unix())) > 120 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !a.filter.Check(authID[:]) {
|
||||
return nil, ErrReplay
|
||||
}
|
||||
|
||||
return v.ticket, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
127
proxy/vmess/aead/authid_test.go
Normal file
127
proxy/vmess/aead/authid_test.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package aead
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateAuthID(t *testing.T) {
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
authid := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
fmt.Println(key)
|
||||
fmt.Println(authid)
|
||||
}
|
||||
|
||||
func TestCreateAuthIDAndDecode(t *testing.T) {
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
authid := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
fmt.Println(key)
|
||||
fmt.Println(authid)
|
||||
|
||||
AuthDecoder := NewAuthIDDecoderHolder()
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
AuthDecoder.AddUser(keyw, "Demo User")
|
||||
res, err := AuthDecoder.Match(authid)
|
||||
fmt.Println(res)
|
||||
fmt.Println(err)
|
||||
assert.Equal(t, "Demo User", res)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCreateAuthIDAndDecode2(t *testing.T) {
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
authid := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
fmt.Println(key)
|
||||
fmt.Println(authid)
|
||||
|
||||
AuthDecoder := NewAuthIDDecoderHolder()
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
AuthDecoder.AddUser(keyw, "Demo User")
|
||||
res, err := AuthDecoder.Match(authid)
|
||||
fmt.Println(res)
|
||||
fmt.Println(err)
|
||||
assert.Equal(t, "Demo User", res)
|
||||
assert.Nil(t, err)
|
||||
|
||||
key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test")
|
||||
authid2 := CreateAuthID(key2, time.Now().Unix())
|
||||
|
||||
res2, err2 := AuthDecoder.Match(authid2)
|
||||
assert.EqualError(t, err2, "user do not exist")
|
||||
assert.Nil(t, res2)
|
||||
}
|
||||
|
||||
func TestCreateAuthIDAndDecodeMassive(t *testing.T) {
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
authid := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
fmt.Println(key)
|
||||
fmt.Println(authid)
|
||||
|
||||
AuthDecoder := NewAuthIDDecoderHolder()
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
AuthDecoder.AddUser(keyw, "Demo User")
|
||||
res, err := AuthDecoder.Match(authid)
|
||||
fmt.Println(res)
|
||||
fmt.Println(err)
|
||||
assert.Equal(t, "Demo User", res)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for i := 0; i <= 10000; i++ {
|
||||
key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i))
|
||||
var keyw2 [16]byte
|
||||
copy(keyw2[:], key2)
|
||||
AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
authid3 := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
res2, err2 := AuthDecoder.Match(authid3)
|
||||
assert.Equal(t, "Demo User", res2)
|
||||
assert.Nil(t, err2)
|
||||
}
|
||||
|
||||
func TestCreateAuthIDAndDecodeSuperMassive(t *testing.T) {
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
authid := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
fmt.Println(key)
|
||||
fmt.Println(authid)
|
||||
|
||||
AuthDecoder := NewAuthIDDecoderHolder()
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
AuthDecoder.AddUser(keyw, "Demo User")
|
||||
res, err := AuthDecoder.Match(authid)
|
||||
fmt.Println(res)
|
||||
fmt.Println(err)
|
||||
assert.Equal(t, "Demo User", res)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for i := 0; i <= 1000000; i++ {
|
||||
key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i))
|
||||
var keyw2 [16]byte
|
||||
copy(keyw2[:], key2)
|
||||
AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
authid3 := CreateAuthID(key, time.Now().Unix())
|
||||
|
||||
before := time.Now()
|
||||
res2, err2 := AuthDecoder.Match(authid3)
|
||||
after := time.Now()
|
||||
assert.Equal(t, "Demo User", res2)
|
||||
assert.Nil(t, err2)
|
||||
|
||||
fmt.Println(after.Sub(before).Seconds())
|
||||
}
|
14
proxy/vmess/aead/consts.go
Normal file
14
proxy/vmess/aead/consts.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package aead
|
||||
|
||||
const (
|
||||
KDFSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption"
|
||||
KDFSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key"
|
||||
KDFSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV"
|
||||
KDFSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key"
|
||||
KDFSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV"
|
||||
KDFSaltConstVMessAEADKDF = "VMess AEAD KDF"
|
||||
KDFSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key"
|
||||
KDFSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce"
|
||||
KDFSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length"
|
||||
KDFSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length"
|
||||
)
|
172
proxy/vmess/aead/encrypt.go
Normal file
172
proxy/vmess/aead/encrypt.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package aead
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
)
|
||||
|
||||
func SealVMessAEADHeader(key [16]byte, data []byte) []byte {
|
||||
generatedAuthID := CreateAuthID(key[:], time.Now().Unix())
|
||||
|
||||
connectionNonce := make([]byte, 8)
|
||||
if _, err := io.ReadFull(rand.Reader, connectionNonce); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
aeadPayloadLengthSerializeBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
headerPayloadDataLen := uint16(len(data))
|
||||
|
||||
common.Must(binary.Write(aeadPayloadLengthSerializeBuffer, binary.BigEndian, headerPayloadDataLen))
|
||||
|
||||
aeadPayloadLengthSerializedByte := aeadPayloadLengthSerializeBuffer.Bytes()
|
||||
var payloadHeaderLengthAEADEncrypted []byte
|
||||
|
||||
{
|
||||
payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce))
|
||||
|
||||
payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12]
|
||||
|
||||
payloadHeaderLengthAEADAESBlock, err := aes.NewCipher(payloadHeaderLengthAEADKey)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderLengthAEADAESBlock)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:])
|
||||
}
|
||||
|
||||
var payloadHeaderAEADEncrypted []byte
|
||||
|
||||
{
|
||||
payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce))
|
||||
|
||||
payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12]
|
||||
|
||||
payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderAEADKey)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:])
|
||||
}
|
||||
|
||||
var outputBuffer = bytes.NewBuffer(nil)
|
||||
|
||||
common.Must2(outputBuffer.Write(generatedAuthID[:])) // 16
|
||||
common.Must2(outputBuffer.Write(payloadHeaderLengthAEADEncrypted)) // 2+16
|
||||
common.Must2(outputBuffer.Write(connectionNonce)) // 8
|
||||
common.Must2(outputBuffer.Write(payloadHeaderAEADEncrypted))
|
||||
|
||||
return outputBuffer.Bytes()
|
||||
}
|
||||
|
||||
func OpenVMessAEADHeader(key [16]byte, authid [16]byte, data io.Reader) ([]byte, bool, int, error) {
|
||||
var payloadHeaderLengthAEADEncrypted [18]byte
|
||||
var nonce [8]byte
|
||||
|
||||
var bytesRead int
|
||||
|
||||
authidCheckValueReadBytesCounts, err := io.ReadFull(data, payloadHeaderLengthAEADEncrypted[:])
|
||||
bytesRead += authidCheckValueReadBytesCounts
|
||||
if err != nil {
|
||||
return nil, false, bytesRead, err
|
||||
}
|
||||
|
||||
nonceReadBytesCounts, err := io.ReadFull(data, nonce[:])
|
||||
bytesRead += nonceReadBytesCounts
|
||||
if err != nil {
|
||||
return nil, false, bytesRead, err
|
||||
}
|
||||
|
||||
// Decrypt Length
|
||||
|
||||
var decryptedAEADHeaderLengthPayloadResult []byte
|
||||
|
||||
{
|
||||
payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(authid[:]), string(nonce[:]))
|
||||
|
||||
payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(authid[:]), string(nonce[:]))[:12]
|
||||
|
||||
payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderLengthAEADKey)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderLengthAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
decryptedAEADHeaderLengthPayload, erropenAEAD := payloadHeaderLengthAEAD.Open(nil, payloadHeaderLengthAEADNonce, payloadHeaderLengthAEADEncrypted[:], authid[:])
|
||||
|
||||
if erropenAEAD != nil {
|
||||
return nil, true, bytesRead, erropenAEAD
|
||||
}
|
||||
|
||||
decryptedAEADHeaderLengthPayloadResult = decryptedAEADHeaderLengthPayload
|
||||
}
|
||||
|
||||
var length uint16
|
||||
|
||||
common.Must(binary.Read(bytes.NewReader(decryptedAEADHeaderLengthPayloadResult), binary.BigEndian, &length))
|
||||
|
||||
var decryptedAEADHeaderPayloadR []byte
|
||||
|
||||
var payloadHeaderAEADEncryptedReadedBytesCounts int
|
||||
|
||||
{
|
||||
payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(authid[:]), string(nonce[:]))
|
||||
|
||||
payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(authid[:]), string(nonce[:]))[:12]
|
||||
|
||||
// 16 == AEAD Tag size
|
||||
payloadHeaderAEADEncrypted := make([]byte, length+16)
|
||||
|
||||
payloadHeaderAEADEncryptedReadedBytesCounts, err = io.ReadFull(data, payloadHeaderAEADEncrypted)
|
||||
bytesRead += payloadHeaderAEADEncryptedReadedBytesCounts
|
||||
if err != nil {
|
||||
return nil, false, bytesRead, err
|
||||
}
|
||||
|
||||
payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderAEADKey)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
decryptedAEADHeaderPayload, erropenAEAD := payloadHeaderAEAD.Open(nil, payloadHeaderAEADNonce, payloadHeaderAEADEncrypted, authid[:])
|
||||
|
||||
if erropenAEAD != nil {
|
||||
return nil, true, bytesRead, erropenAEAD
|
||||
}
|
||||
|
||||
decryptedAEADHeaderPayloadR = decryptedAEADHeaderPayload
|
||||
}
|
||||
|
||||
return decryptedAEADHeaderPayloadR, false, bytesRead, nil
|
||||
}
|
104
proxy/vmess/aead/encrypt_test.go
Normal file
104
proxy/vmess/aead/encrypt_test.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package aead
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOpenVMessAEADHeader(t *testing.T) {
|
||||
TestHeader := []byte("Test Header")
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
sealed := SealVMessAEADHeader(keyw, TestHeader)
|
||||
|
||||
var AEADR = bytes.NewReader(sealed)
|
||||
|
||||
var authid [16]byte
|
||||
|
||||
io.ReadFull(AEADR, authid[:])
|
||||
|
||||
out, _, _, err := OpenVMessAEADHeader(keyw, authid, AEADR)
|
||||
|
||||
fmt.Println(string(out))
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func TestOpenVMessAEADHeader2(t *testing.T) {
|
||||
TestHeader := []byte("Test Header")
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
sealed := SealVMessAEADHeader(keyw, TestHeader)
|
||||
|
||||
var AEADR = bytes.NewReader(sealed)
|
||||
|
||||
var authid [16]byte
|
||||
|
||||
io.ReadFull(AEADR, authid[:])
|
||||
|
||||
out, _, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR)
|
||||
assert.Equal(t, len(sealed)-16-AEADR.Len(), readen)
|
||||
assert.Equal(t, string(TestHeader), string(out))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestOpenVMessAEADHeader4(t *testing.T) {
|
||||
for i := 0; i <= 60; i++ {
|
||||
TestHeader := []byte("Test Header")
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
sealed := SealVMessAEADHeader(keyw, TestHeader)
|
||||
var sealedm [16]byte
|
||||
copy(sealedm[:], sealed)
|
||||
sealed[i] ^= 0xff
|
||||
var AEADR = bytes.NewReader(sealed)
|
||||
|
||||
var authid [16]byte
|
||||
|
||||
io.ReadFull(AEADR, authid[:])
|
||||
|
||||
out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR)
|
||||
assert.Equal(t, len(sealed)-16-AEADR.Len(), readen)
|
||||
assert.Equal(t, true, drain)
|
||||
assert.NotNil(t, err)
|
||||
if err == nil {
|
||||
fmt.Println(">")
|
||||
}
|
||||
assert.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenVMessAEADHeader4Massive(t *testing.T) {
|
||||
for j := 0; j < 1000; j++ {
|
||||
for i := 0; i <= 60; i++ {
|
||||
TestHeader := []byte("Test Header")
|
||||
key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test")
|
||||
var keyw [16]byte
|
||||
copy(keyw[:], key)
|
||||
sealed := SealVMessAEADHeader(keyw, TestHeader)
|
||||
var sealedm [16]byte
|
||||
copy(sealedm[:], sealed)
|
||||
sealed[i] ^= 0xff
|
||||
var AEADR = bytes.NewReader(sealed)
|
||||
|
||||
var authid [16]byte
|
||||
|
||||
io.ReadFull(AEADR, authid[:])
|
||||
|
||||
out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR)
|
||||
assert.Equal(t, len(sealed)-16-AEADR.Len(), readen)
|
||||
assert.Equal(t, true, drain)
|
||||
assert.NotNil(t, err)
|
||||
if err == nil {
|
||||
fmt.Println(">")
|
||||
}
|
||||
assert.Nil(t, out)
|
||||
}
|
||||
}
|
||||
}
|
24
proxy/vmess/aead/kdf.go
Normal file
24
proxy/vmess/aead/kdf.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package aead
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
)
|
||||
|
||||
func KDF(key []byte, path ...string) []byte {
|
||||
hmacf := hmac.New(sha256.New, []byte(KDFSaltConstVMessAEADKDF))
|
||||
|
||||
for _, v := range path {
|
||||
hmacf = hmac.New(func() hash.Hash {
|
||||
return hmacf
|
||||
}, []byte(v))
|
||||
}
|
||||
hmacf.Write(key)
|
||||
return hmacf.Sum(nil)
|
||||
}
|
||||
|
||||
func KDF16(key []byte, path ...string) []byte {
|
||||
r := KDF(key, path...)
|
||||
return r[:16]
|
||||
}
|
119
proxy/vmess/encoding/auth.go
Normal file
119
proxy/vmess/encoding/auth.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// Authenticate authenticates a byte array using Fnv hash.
|
||||
func Authenticate(b []byte) uint32 {
|
||||
fnv1hash := fnv.New32a()
|
||||
common.Must2(fnv1hash.Write(b))
|
||||
return fnv1hash.Sum32()
|
||||
}
|
||||
|
||||
type NoOpAuthenticator struct{}
|
||||
|
||||
func (NoOpAuthenticator) NonceSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (NoOpAuthenticator) Overhead() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Seal implements AEAD.Seal().
|
||||
func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
return append(dst[:0], plaintext...)
|
||||
}
|
||||
|
||||
// Open implements AEAD.Open().
|
||||
func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
return append(dst[:0], ciphertext...), nil
|
||||
}
|
||||
|
||||
// FnvAuthenticator is an AEAD based on Fnv hash.
|
||||
type FnvAuthenticator struct {
|
||||
}
|
||||
|
||||
// NonceSize implements AEAD.NonceSize().
|
||||
func (*FnvAuthenticator) NonceSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Overhead impelements AEAD.Overhead().
|
||||
func (*FnvAuthenticator) Overhead() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
// Seal implements AEAD.Seal().
|
||||
func (*FnvAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
dst = append(dst, 0, 0, 0, 0)
|
||||
binary.BigEndian.PutUint32(dst, Authenticate(plaintext))
|
||||
return append(dst, plaintext...)
|
||||
}
|
||||
|
||||
// Open implements AEAD.Open().
|
||||
func (*FnvAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
if binary.BigEndian.Uint32(ciphertext[:4]) != Authenticate(ciphertext[4:]) {
|
||||
return dst, newError("invalid authentication")
|
||||
}
|
||||
return append(dst, ciphertext[4:]...), nil
|
||||
}
|
||||
|
||||
// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array.
|
||||
func GenerateChacha20Poly1305Key(b []byte) []byte {
|
||||
key := make([]byte, 32)
|
||||
t := md5.Sum(b)
|
||||
copy(key, t[:])
|
||||
t = md5.Sum(key[:16])
|
||||
copy(key[16:], t[:])
|
||||
return key
|
||||
}
|
||||
|
||||
type ShakeSizeParser struct {
|
||||
shake sha3.ShakeHash
|
||||
buffer [2]byte
|
||||
}
|
||||
|
||||
func NewShakeSizeParser(nonce []byte) *ShakeSizeParser {
|
||||
shake := sha3.NewShake128()
|
||||
common.Must2(shake.Write(nonce))
|
||||
return &ShakeSizeParser{
|
||||
shake: shake,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ShakeSizeParser) SizeBytes() int32 {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) next() uint16 {
|
||||
common.Must2(s.shake.Read(s.buffer[:]))
|
||||
return binary.BigEndian.Uint16(s.buffer[:])
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) Decode(b []byte) (uint16, error) {
|
||||
mask := s.next()
|
||||
size := binary.BigEndian.Uint16(b)
|
||||
return mask ^ size, nil
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) Encode(size uint16, b []byte) []byte {
|
||||
mask := s.next()
|
||||
binary.BigEndian.PutUint16(b, mask^size)
|
||||
return b[:2]
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) NextPaddingLen() uint16 {
|
||||
return s.next() % 64
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) MaxPaddingLen() uint16 {
|
||||
return 64
|
||||
}
|
27
proxy/vmess/encoding/auth_test.go
Normal file
27
proxy/vmess/encoding/auth_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func TestFnvAuth(t *testing.T) {
|
||||
fnvAuth := new(FnvAuthenticator)
|
||||
|
||||
expectedText := make([]byte, 256)
|
||||
_, err := rand.Read(expectedText)
|
||||
common.Must(err)
|
||||
|
||||
buffer := make([]byte, 512)
|
||||
b := fnvAuth.Seal(buffer[:0], nil, expectedText, nil)
|
||||
b, err = fnvAuth.Open(buffer[:0], nil, b, nil)
|
||||
common.Must(err)
|
||||
if r := cmp.Diff(b, expectedText); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
338
proxy/vmess/encoding/client.go
Normal file
338
proxy/vmess/encoding/client.go
Normal file
|
@ -0,0 +1,338 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/bitmask"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/crypto"
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead"
|
||||
)
|
||||
|
||||
func hashTimestamp(h hash.Hash, t protocol.Timestamp) []byte {
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// ClientSession stores connection session info for VMess client.
|
||||
type ClientSession struct {
|
||||
isAEAD bool
|
||||
idHash protocol.IDHash
|
||||
requestBodyKey [16]byte
|
||||
requestBodyIV [16]byte
|
||||
responseBodyKey [16]byte
|
||||
responseBodyIV [16]byte
|
||||
responseReader io.Reader
|
||||
responseHeader byte
|
||||
}
|
||||
|
||||
// NewClientSession creates a new ClientSession.
|
||||
func NewClientSession(ctx context.Context, isAEAD bool, idHash protocol.IDHash) *ClientSession {
|
||||
session := &ClientSession{
|
||||
isAEAD: isAEAD,
|
||||
idHash: idHash,
|
||||
}
|
||||
|
||||
randomBytes := make([]byte, 33) // 16 + 16 + 1
|
||||
common.Must2(rand.Read(randomBytes))
|
||||
copy(session.requestBodyKey[:], randomBytes[:16])
|
||||
copy(session.requestBodyIV[:], randomBytes[16:32])
|
||||
session.responseHeader = randomBytes[32]
|
||||
|
||||
if !session.isAEAD {
|
||||
session.responseBodyKey = md5.Sum(session.requestBodyKey[:])
|
||||
session.responseBodyIV = md5.Sum(session.requestBodyIV[:])
|
||||
} else {
|
||||
BodyKey := sha256.Sum256(session.requestBodyKey[:])
|
||||
copy(session.responseBodyKey[:], BodyKey[:16])
|
||||
BodyIV := sha256.Sum256(session.requestBodyIV[:])
|
||||
copy(session.responseBodyIV[:], BodyIV[:16])
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error {
|
||||
timestamp := protocol.NewTimestampGenerator(protocol.NowTime(), 30)()
|
||||
account := header.User.Account.(*vmess.MemoryAccount)
|
||||
if !c.isAEAD {
|
||||
idHash := c.idHash(account.AnyValidID().Bytes())
|
||||
common.Must2(serial.WriteUint64(idHash, uint64(timestamp)))
|
||||
common.Must2(writer.Write(idHash.Sum(nil)))
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
common.Must(buffer.WriteByte(Version))
|
||||
common.Must2(buffer.Write(c.requestBodyIV[:]))
|
||||
common.Must2(buffer.Write(c.requestBodyKey[:]))
|
||||
common.Must(buffer.WriteByte(c.responseHeader))
|
||||
common.Must(buffer.WriteByte(byte(header.Option)))
|
||||
|
||||
paddingLen := dice.Roll(16)
|
||||
security := byte(paddingLen<<4) | byte(header.Security)
|
||||
common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)}))
|
||||
|
||||
if header.Command != protocol.RequestCommandMux {
|
||||
if err := addrParser.WriteAddressPort(buffer, header.Address, header.Port); err != nil {
|
||||
return newError("failed to writer address and port").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if paddingLen > 0 {
|
||||
common.Must2(buffer.ReadFullFrom(rand.Reader, int32(paddingLen)))
|
||||
}
|
||||
|
||||
{
|
||||
fnv1a := fnv.New32a()
|
||||
common.Must2(fnv1a.Write(buffer.Bytes()))
|
||||
hashBytes := buffer.Extend(int32(fnv1a.Size()))
|
||||
fnv1a.Sum(hashBytes[:0])
|
||||
}
|
||||
|
||||
if !c.isAEAD {
|
||||
iv := hashTimestamp(md5.New(), timestamp)
|
||||
aesStream := crypto.NewAesEncryptionStream(account.ID.CmdKey(), iv)
|
||||
aesStream.XORKeyStream(buffer.Bytes(), buffer.Bytes())
|
||||
common.Must2(writer.Write(buffer.Bytes()))
|
||||
} else {
|
||||
var fixedLengthCmdKey [16]byte
|
||||
copy(fixedLengthCmdKey[:], account.ID.CmdKey())
|
||||
vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes())
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(vmessout)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
|
||||
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(c.requestBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer)
|
||||
}
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
|
||||
return buf.NewWriter(writer)
|
||||
case protocol.SecurityType_LEGACY:
|
||||
aesStream := crypto.NewAesEncryptionStream(c.requestBodyKey[:], c.requestBodyIV[:])
|
||||
cryptionWriter := crypto.NewCryptionWriter(aesStream, writer)
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, cryptionWriter, request.Command.TransferType(), padding)
|
||||
}
|
||||
|
||||
return &buf.SequentialWriter{Writer: cryptionWriter}
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:]))
|
||||
common.Must(err)
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) {
|
||||
if !c.isAEAD {
|
||||
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
|
||||
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
|
||||
} else {
|
||||
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
|
||||
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
|
||||
|
||||
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
var aeadEncryptedResponseHeaderLength [18]byte
|
||||
var decryptedResponseHeaderLength int
|
||||
var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16
|
||||
|
||||
if _, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil {
|
||||
return nil, newError("Unable to Read Header Len").Base(err)
|
||||
}
|
||||
if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil {
|
||||
return nil, newError("Failed To Decrypt Length").Base(err)
|
||||
} else {
|
||||
common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer))
|
||||
decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer)
|
||||
}
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
|
||||
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
|
||||
|
||||
if _, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil {
|
||||
return nil, newError("Unable to Read Header Data").Base(err)
|
||||
}
|
||||
|
||||
if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil {
|
||||
return nil, newError("Failed To Decrypt Payload").Base(err)
|
||||
} else {
|
||||
c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
buffer := buf.StackNew()
|
||||
defer buffer.Release()
|
||||
|
||||
if _, err := buffer.ReadFullFrom(c.responseReader, 4); err != nil {
|
||||
return nil, newError("failed to read response header").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
if buffer.Byte(0) != c.responseHeader {
|
||||
return nil, newError("unexpected response header. Expecting ", int(c.responseHeader), " but actually ", int(buffer.Byte(0)))
|
||||
}
|
||||
|
||||
header := &protocol.ResponseHeader{
|
||||
Option: bitmask.Byte(buffer.Byte(1)),
|
||||
}
|
||||
|
||||
if buffer.Byte(2) != 0 {
|
||||
cmdID := buffer.Byte(2)
|
||||
dataLen := int32(buffer.Byte(3))
|
||||
|
||||
buffer.Clear()
|
||||
if _, err := buffer.ReadFullFrom(c.responseReader, dataLen); err != nil {
|
||||
return nil, newError("failed to read response command").Base(err)
|
||||
}
|
||||
command, err := UnmarshalCommand(cmdID, buffer.Bytes())
|
||||
if err == nil {
|
||||
header.Command = command
|
||||
}
|
||||
}
|
||||
if c.isAEAD {
|
||||
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
|
||||
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
|
||||
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(c.responseBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
|
||||
return buf.NewReader(reader)
|
||||
case protocol.SecurityType_LEGACY:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, c.responseReader, request.Command.TransferType(), padding)
|
||||
}
|
||||
|
||||
return buf.NewReader(c.responseReader)
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.responseBodyKey[:])
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.responseBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateChunkNonce(nonce []byte, size uint32) crypto.BytesGenerator {
|
||||
c := append([]byte(nil), nonce...)
|
||||
count := uint16(0)
|
||||
return func() []byte {
|
||||
binary.BigEndian.PutUint16(c, count)
|
||||
count++
|
||||
return c[:size]
|
||||
}
|
||||
}
|
148
proxy/vmess/encoding/commands.go
Normal file
148
proxy/vmess/encoding/commands.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandTypeMismatch = newError("Command type mismatch.")
|
||||
ErrUnknownCommand = newError("Unknown command.")
|
||||
ErrCommandTooLarge = newError("Command too large.")
|
||||
)
|
||||
|
||||
func MarshalCommand(command interface{}, writer io.Writer) error {
|
||||
if command == nil {
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
|
||||
var cmdID byte
|
||||
var factory CommandFactory
|
||||
switch command.(type) {
|
||||
case *protocol.CommandSwitchAccount:
|
||||
factory = new(CommandSwitchAccountFactory)
|
||||
cmdID = 1
|
||||
default:
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
err := factory.Marshal(command, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auth := Authenticate(buffer.Bytes())
|
||||
length := buffer.Len() + 4
|
||||
if length > 255 {
|
||||
return ErrCommandTooLarge
|
||||
}
|
||||
|
||||
common.Must2(writer.Write([]byte{cmdID, byte(length), byte(auth >> 24), byte(auth >> 16), byte(auth >> 8), byte(auth)}))
|
||||
common.Must2(writer.Write(buffer.Bytes()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalCommand(cmdID byte, data []byte) (protocol.ResponseCommand, error) {
|
||||
if len(data) <= 4 {
|
||||
return nil, newError("insufficient length")
|
||||
}
|
||||
expectedAuth := Authenticate(data[4:])
|
||||
actualAuth := binary.BigEndian.Uint32(data[:4])
|
||||
if expectedAuth != actualAuth {
|
||||
return nil, newError("invalid auth")
|
||||
}
|
||||
|
||||
var factory CommandFactory
|
||||
switch cmdID {
|
||||
case 1:
|
||||
factory = new(CommandSwitchAccountFactory)
|
||||
default:
|
||||
return nil, ErrUnknownCommand
|
||||
}
|
||||
return factory.Unmarshal(data[4:])
|
||||
}
|
||||
|
||||
type CommandFactory interface {
|
||||
Marshal(command interface{}, writer io.Writer) error
|
||||
Unmarshal(data []byte) (interface{}, error)
|
||||
}
|
||||
|
||||
type CommandSwitchAccountFactory struct {
|
||||
}
|
||||
|
||||
func (f *CommandSwitchAccountFactory) Marshal(command interface{}, writer io.Writer) error {
|
||||
cmd, ok := command.(*protocol.CommandSwitchAccount)
|
||||
if !ok {
|
||||
return ErrCommandTypeMismatch
|
||||
}
|
||||
|
||||
hostStr := ""
|
||||
if cmd.Host != nil {
|
||||
hostStr = cmd.Host.String()
|
||||
}
|
||||
common.Must2(writer.Write([]byte{byte(len(hostStr))}))
|
||||
|
||||
if len(hostStr) > 0 {
|
||||
common.Must2(writer.Write([]byte(hostStr)))
|
||||
}
|
||||
|
||||
common.Must2(serial.WriteUint16(writer, cmd.Port.Value()))
|
||||
|
||||
idBytes := cmd.ID.Bytes()
|
||||
common.Must2(writer.Write(idBytes))
|
||||
common.Must2(serial.WriteUint16(writer, cmd.AlterIds))
|
||||
common.Must2(writer.Write([]byte{byte(cmd.Level)}))
|
||||
|
||||
common.Must2(writer.Write([]byte{cmd.ValidMin}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *CommandSwitchAccountFactory) Unmarshal(data []byte) (interface{}, error) {
|
||||
cmd := new(protocol.CommandSwitchAccount)
|
||||
if len(data) == 0 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
lenHost := int(data[0])
|
||||
if len(data) < lenHost+1 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
if lenHost > 0 {
|
||||
cmd.Host = net.ParseAddress(string(data[1 : 1+lenHost]))
|
||||
}
|
||||
portStart := 1 + lenHost
|
||||
if len(data) < portStart+2 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.Port = net.PortFromBytes(data[portStart : portStart+2])
|
||||
idStart := portStart + 2
|
||||
if len(data) < idStart+16 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.ID, _ = uuid.ParseBytes(data[idStart : idStart+16])
|
||||
alterIDStart := idStart + 16
|
||||
if len(data) < alterIDStart+2 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.AlterIds = binary.BigEndian.Uint16(data[alterIDStart : alterIDStart+2])
|
||||
levelStart := alterIDStart + 2
|
||||
if len(data) < levelStart+1 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.Level = uint32(data[levelStart])
|
||||
timeStart := levelStart + 1
|
||||
if len(data) < timeStart {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.ValidMin = data[timeStart]
|
||||
return cmd, nil
|
||||
}
|
37
proxy/vmess/encoding/commands_test.go
Normal file
37
proxy/vmess/encoding/commands_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func TestSwitchAccount(t *testing.T) {
|
||||
sa := &protocol.CommandSwitchAccount{
|
||||
Port: 1234,
|
||||
ID: uuid.New(),
|
||||
AlterIds: 1024,
|
||||
Level: 128,
|
||||
ValidMin: 16,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
common.Must(MarshalCommand(sa, buffer))
|
||||
|
||||
cmd, err := UnmarshalCommand(1, buffer.BytesFrom(2))
|
||||
common.Must(err)
|
||||
|
||||
sa2, ok := cmd.(*protocol.CommandSwitchAccount)
|
||||
if !ok {
|
||||
t.Fatal("failed to convert command to CommandSwitchAccount")
|
||||
}
|
||||
if r := cmp.Diff(sa2, sa); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
19
proxy/vmess/encoding/encoding.go
Normal file
19
proxy/vmess/encoding/encoding.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
||||
|
||||
const (
|
||||
Version = byte(1)
|
||||
)
|
||||
|
||||
var addrParser = protocol.NewAddressParser(
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4),
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain),
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6),
|
||||
protocol.PortThenAddress(),
|
||||
)
|
157
proxy/vmess/encoding/encoding_test.go
Normal file
157
proxy/vmess/encoding/encoding_test.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func toAccount(a *vmess.Account) protocol.Account {
|
||||
account, err := a.AsAccount()
|
||||
common.Must(err)
|
||||
return account
|
||||
}
|
||||
|
||||
func TestRequestSerialization(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommandTCP,
|
||||
Address: net.DomainAddress("www.example.com"),
|
||||
Port: net.Port(443),
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
actualRequest, err := server.DecodeRequestHeader(buffer)
|
||||
common.Must(err)
|
||||
|
||||
if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
|
||||
_, err = server.DecodeRequestHeader(buffer2)
|
||||
// anti replay attack
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidRequest(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommand(100),
|
||||
Address: net.DomainAddress("www.example.com"),
|
||||
Port: net.Port(443),
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
_, err := server.DecodeRequestHeader(buffer)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMuxRequest(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommandMux,
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
Address: net.DomainAddress("v1.mux.cool"),
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
actualRequest, err := server.DecodeRequestHeader(buffer)
|
||||
common.Must(err)
|
||||
|
||||
if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
9
proxy/vmess/encoding/errors.generated.go
Normal file
9
proxy/vmess/encoding/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package encoding
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
492
proxy/vmess/encoding/server.go
Normal file
492
proxy/vmess/encoding/server.go
Normal file
|
@ -0,0 +1,492 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/bitmask"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/crypto"
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/task"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
type sessionID struct {
|
||||
user [16]byte
|
||||
key [16]byte
|
||||
nonce [16]byte
|
||||
}
|
||||
|
||||
// SessionHistory keeps track of historical session ids, to prevent replay attacks.
|
||||
type SessionHistory struct {
|
||||
sync.RWMutex
|
||||
cache map[sessionID]time.Time
|
||||
task *task.Periodic
|
||||
}
|
||||
|
||||
// NewSessionHistory creates a new SessionHistory object.
|
||||
func NewSessionHistory() *SessionHistory {
|
||||
h := &SessionHistory{
|
||||
cache: make(map[sessionID]time.Time, 128),
|
||||
}
|
||||
h.task = &task.Periodic{
|
||||
Interval: time.Second * 30,
|
||||
Execute: h.removeExpiredEntries,
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (h *SessionHistory) Close() error {
|
||||
return h.task.Close()
|
||||
}
|
||||
|
||||
func (h *SessionHistory) addIfNotExits(session sessionID) bool {
|
||||
h.Lock()
|
||||
|
||||
if expire, found := h.cache[session]; found && expire.After(time.Now()) {
|
||||
h.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
h.cache[session] = time.Now().Add(time.Minute * 3)
|
||||
h.Unlock()
|
||||
common.Must(h.task.Start())
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *SessionHistory) removeExpiredEntries() error {
|
||||
now := time.Now()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if len(h.cache) == 0 {
|
||||
return newError("nothing to do")
|
||||
}
|
||||
|
||||
for session, expire := range h.cache {
|
||||
if expire.Before(now) {
|
||||
delete(h.cache, session)
|
||||
}
|
||||
}
|
||||
|
||||
if len(h.cache) == 0 {
|
||||
h.cache = make(map[sessionID]time.Time, 128)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerSession keeps information for a session in VMess server.
|
||||
type ServerSession struct {
|
||||
userValidator *vmess.TimedUserValidator
|
||||
sessionHistory *SessionHistory
|
||||
requestBodyKey [16]byte
|
||||
requestBodyIV [16]byte
|
||||
responseBodyKey [16]byte
|
||||
responseBodyIV [16]byte
|
||||
responseWriter io.Writer
|
||||
responseHeader byte
|
||||
|
||||
isAEADRequest bool
|
||||
|
||||
isAEADForced bool
|
||||
}
|
||||
|
||||
// NewServerSession creates a new ServerSession, using the given UserValidator.
|
||||
// The ServerSession instance doesn't take ownership of the validator.
|
||||
func NewServerSession(validator *vmess.TimedUserValidator, sessionHistory *SessionHistory) *ServerSession {
|
||||
return &ServerSession{
|
||||
userValidator: validator,
|
||||
sessionHistory: sessionHistory,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSecurityType(b byte) protocol.SecurityType {
|
||||
if _, f := protocol.SecurityType_name[int32(b)]; f {
|
||||
st := protocol.SecurityType(b)
|
||||
// For backward compatibility.
|
||||
if st == protocol.SecurityType_UNKNOWN {
|
||||
st = protocol.SecurityType_LEGACY
|
||||
}
|
||||
return st
|
||||
}
|
||||
return protocol.SecurityType_UNKNOWN
|
||||
}
|
||||
|
||||
// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream.
|
||||
func (s *ServerSession) DecodeRequestHeader(reader io.Reader) (*protocol.RequestHeader, error) {
|
||||
buffer := buf.New()
|
||||
behaviorRand := dice.NewDeterministicDice(int64(s.userValidator.GetBehaviorSeed()))
|
||||
BaseDrainSize := behaviorRand.Roll(3266)
|
||||
RandDrainMax := behaviorRand.Roll(64) + 1
|
||||
RandDrainRolled := dice.Roll(RandDrainMax)
|
||||
DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled
|
||||
readSizeRemain := DrainSize
|
||||
|
||||
drainConnection := func(e error) error {
|
||||
// We read a deterministic generated length of data before closing the connection to offset padding read pattern
|
||||
readSizeRemain -= int(buffer.Len())
|
||||
if readSizeRemain > 0 {
|
||||
err := s.DrainConnN(reader, readSizeRemain)
|
||||
if err != nil {
|
||||
return newError("failed to drain connection DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(err).Base(e)
|
||||
}
|
||||
return newError("connection drained DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
defer func() {
|
||||
buffer.Release()
|
||||
}()
|
||||
|
||||
if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil {
|
||||
return nil, newError("failed to read request header").Base(err)
|
||||
}
|
||||
|
||||
var decryptor io.Reader
|
||||
var vmessAccount *vmess.MemoryAccount
|
||||
|
||||
user, foundAEAD, errorAEAD := s.userValidator.GetAEAD(buffer.Bytes())
|
||||
|
||||
var fixedSizeAuthID [16]byte
|
||||
copy(fixedSizeAuthID[:], buffer.Bytes())
|
||||
|
||||
switch {
|
||||
case foundAEAD:
|
||||
vmessAccount = user.Account.(*vmess.MemoryAccount)
|
||||
var fixedSizeCmdKey [16]byte
|
||||
copy(fixedSizeCmdKey[:], vmessAccount.ID.CmdKey())
|
||||
aeadData, shouldDrain, bytesRead, errorReason := vmessaead.OpenVMessAEADHeader(fixedSizeCmdKey, fixedSizeAuthID, reader)
|
||||
if errorReason != nil {
|
||||
if shouldDrain {
|
||||
readSizeRemain -= bytesRead
|
||||
return nil, drainConnection(newError("AEAD read failed").Base(errorReason))
|
||||
} else {
|
||||
return nil, drainConnection(newError("AEAD read failed, drain skipped").Base(errorReason))
|
||||
}
|
||||
}
|
||||
decryptor = bytes.NewReader(aeadData)
|
||||
s.isAEADRequest = true
|
||||
|
||||
case !s.isAEADForced && errorAEAD == vmessaead.ErrNotFound:
|
||||
userLegacy, timestamp, valid, userValidationError := s.userValidator.Get(buffer.Bytes())
|
||||
if !valid || userValidationError != nil {
|
||||
return nil, drainConnection(newError("invalid user").Base(userValidationError))
|
||||
}
|
||||
user = userLegacy
|
||||
iv := hashTimestamp(md5.New(), timestamp)
|
||||
vmessAccount = userLegacy.Account.(*vmess.MemoryAccount)
|
||||
|
||||
aesStream := crypto.NewAesDecryptionStream(vmessAccount.ID.CmdKey(), iv)
|
||||
decryptor = crypto.NewCryptionReader(aesStream, reader)
|
||||
|
||||
default:
|
||||
return nil, drainConnection(newError("invalid user").Base(errorAEAD))
|
||||
}
|
||||
|
||||
readSizeRemain -= int(buffer.Len())
|
||||
buffer.Clear()
|
||||
if _, err := buffer.ReadFullFrom(decryptor, 38); err != nil {
|
||||
return nil, newError("failed to read request header").Base(err)
|
||||
}
|
||||
|
||||
request := &protocol.RequestHeader{
|
||||
User: user,
|
||||
Version: buffer.Byte(0),
|
||||
}
|
||||
|
||||
copy(s.requestBodyIV[:], buffer.BytesRange(1, 17)) // 16 bytes
|
||||
copy(s.requestBodyKey[:], buffer.BytesRange(17, 33)) // 16 bytes
|
||||
var sid sessionID
|
||||
copy(sid.user[:], vmessAccount.ID.Bytes())
|
||||
sid.key = s.requestBodyKey
|
||||
sid.nonce = s.requestBodyIV
|
||||
if !s.sessionHistory.addIfNotExits(sid) {
|
||||
if !s.isAEADRequest {
|
||||
drainErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if drainErr != nil {
|
||||
return nil, drainConnection(newError("duplicated session id, possibly under replay attack, and failed to taint userHash").Base(drainErr))
|
||||
}
|
||||
return nil, drainConnection(newError("duplicated session id, possibly under replay attack, userHash tainted"))
|
||||
} else {
|
||||
return nil, newError("duplicated session id, possibly under replay attack, but this is a AEAD request")
|
||||
}
|
||||
}
|
||||
|
||||
s.responseHeader = buffer.Byte(33) // 1 byte
|
||||
request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte
|
||||
paddingLen := int(buffer.Byte(35) >> 4)
|
||||
request.Security = parseSecurityType(buffer.Byte(35) & 0x0F)
|
||||
// 1 bytes reserved
|
||||
request.Command = protocol.RequestCommand(buffer.Byte(37))
|
||||
|
||||
switch request.Command {
|
||||
case protocol.RequestCommandMux:
|
||||
request.Address = net.DomainAddress("v1.mux.cool")
|
||||
request.Port = 0
|
||||
|
||||
case protocol.RequestCommandTCP, protocol.RequestCommandUDP:
|
||||
if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil {
|
||||
request.Address = addr
|
||||
request.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
if paddingLen > 0 {
|
||||
if _, err := buffer.ReadFullFrom(decryptor, int32(paddingLen)); err != nil {
|
||||
if !s.isAEADRequest {
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
return nil, newError("failed to read padding, failed to taint userHash").Base(burnErr).Base(err)
|
||||
}
|
||||
return nil, newError("failed to read padding, userHash tainted").Base(err)
|
||||
}
|
||||
return nil, newError("failed to read padding").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil {
|
||||
if !s.isAEADRequest {
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
return nil, newError("failed to read checksum, failed to taint userHash").Base(burnErr).Base(err)
|
||||
}
|
||||
return nil, newError("failed to read checksum, userHash tainted").Base(err)
|
||||
}
|
||||
return nil, newError("failed to read checksum").Base(err)
|
||||
}
|
||||
|
||||
fnv1a := fnv.New32a()
|
||||
common.Must2(fnv1a.Write(buffer.BytesTo(-4)))
|
||||
actualHash := fnv1a.Sum32()
|
||||
expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4))
|
||||
|
||||
if actualHash != expectedHash {
|
||||
if !s.isAEADRequest {
|
||||
Autherr := newError("invalid auth, legacy userHash tainted")
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
Autherr = newError("invalid auth, can't taint legacy userHash").Base(burnErr)
|
||||
}
|
||||
// It is possible that we are under attack described in https://github.com/xray/xray-core/issues/2523
|
||||
return nil, drainConnection(Autherr)
|
||||
} else {
|
||||
return nil, newError("invalid auth, but this is a AEAD request")
|
||||
}
|
||||
}
|
||||
|
||||
if request.Address == nil {
|
||||
return nil, newError("invalid remote address")
|
||||
}
|
||||
|
||||
if request.Security == protocol.SecurityType_UNKNOWN || request.Security == protocol.SecurityType_AUTO {
|
||||
return nil, newError("unknown security type: ", request.Security)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// DecodeRequestBody returns Reader from which caller can fetch decrypted body.
|
||||
func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
|
||||
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(s.requestBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
return buf.NewReader(reader)
|
||||
|
||||
case protocol.SecurityType_LEGACY:
|
||||
aesStream := crypto.NewAesDecryptionStream(s.requestBodyKey[:], s.requestBodyIV[:])
|
||||
cryptionReader := crypto.NewCryptionReader(aesStream, reader)
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, cryptionReader, request.Command.TransferType(), padding)
|
||||
}
|
||||
return buf.NewReader(cryptionReader)
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.requestBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeResponseHeader writes encoded response header into the given writer.
|
||||
func (s *ServerSession) EncodeResponseHeader(header *protocol.ResponseHeader, writer io.Writer) {
|
||||
var encryptionWriter io.Writer
|
||||
if !s.isAEADRequest {
|
||||
s.responseBodyKey = md5.Sum(s.requestBodyKey[:])
|
||||
s.responseBodyIV = md5.Sum(s.requestBodyIV[:])
|
||||
} else {
|
||||
BodyKey := sha256.Sum256(s.requestBodyKey[:])
|
||||
copy(s.responseBodyKey[:], BodyKey[:16])
|
||||
BodyIV := sha256.Sum256(s.requestBodyIV[:])
|
||||
copy(s.responseBodyIV[:], BodyIV[:16])
|
||||
}
|
||||
|
||||
aesStream := crypto.NewAesEncryptionStream(s.responseBodyKey[:], s.responseBodyIV[:])
|
||||
encryptionWriter = crypto.NewCryptionWriter(aesStream, writer)
|
||||
s.responseWriter = encryptionWriter
|
||||
|
||||
aeadEncryptedHeaderBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
if s.isAEADRequest {
|
||||
encryptionWriter = aeadEncryptedHeaderBuffer
|
||||
}
|
||||
|
||||
common.Must2(encryptionWriter.Write([]byte{s.responseHeader, byte(header.Option)}))
|
||||
err := MarshalCommand(header.Command, encryptionWriter)
|
||||
if err != nil {
|
||||
common.Must2(encryptionWriter.Write([]byte{0x00, 0x00}))
|
||||
}
|
||||
|
||||
if s.isAEADRequest {
|
||||
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
|
||||
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
|
||||
|
||||
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
aeadResponseHeaderLengthEncryptionBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
decryptedResponseHeaderLengthBinaryDeserializeBuffer := uint16(aeadEncryptedHeaderBuffer.Len())
|
||||
|
||||
common.Must(binary.Write(aeadResponseHeaderLengthEncryptionBuffer, binary.BigEndian, decryptedResponseHeaderLengthBinaryDeserializeBuffer))
|
||||
|
||||
AEADEncryptedLength := aeadResponseHeaderLengthEncryptionAEAD.Seal(nil, aeadResponseHeaderLengthEncryptionIV, aeadResponseHeaderLengthEncryptionBuffer.Bytes(), nil)
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(AEADEncryptedLength)))
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
|
||||
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
aeadEncryptedHeaderPayload := aeadResponseHeaderPayloadEncryptionAEAD.Seal(nil, aeadResponseHeaderPayloadEncryptionIV, aeadEncryptedHeaderBuffer.Bytes(), nil)
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(aeadEncryptedHeaderPayload)))
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeResponseBody returns a Writer that auto-encrypt content written by caller.
|
||||
func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
|
||||
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(s.responseBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
return buf.NewWriter(writer)
|
||||
|
||||
case protocol.SecurityType_LEGACY:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, s.responseWriter, request.Command.TransferType(), padding)
|
||||
}
|
||||
return &buf.SequentialWriter{Writer: s.responseWriter}
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.responseBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.responseBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerSession) DrainConnN(reader io.Reader, n int) error {
|
||||
_, err := io.CopyN(ioutil.Discard, reader, int64(n))
|
||||
return err
|
||||
}
|
9
proxy/vmess/errors.generated.go
Normal file
9
proxy/vmess/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package vmess
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
11
proxy/vmess/inbound/config.go
Normal file
11
proxy/vmess/inbound/config.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// +build !confonly
|
||||
|
||||
package inbound
|
||||
|
||||
// GetDefaultValue returns default settings of DefaultConfig.
|
||||
func (c *Config) GetDefaultValue() *DefaultConfig {
|
||||
if c.GetDefault() == nil {
|
||||
return &DefaultConfig{}
|
||||
}
|
||||
return c.Default
|
||||
}
|
333
proxy/vmess/inbound/config.pb.go
Normal file
333
proxy/vmess/inbound/config.pb.go
Normal file
|
@ -0,0 +1,333 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.14.0
|
||||
// source: proxy/vmess/inbound/config.proto
|
||||
|
||||
package inbound
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
protocol "github.com/xtls/xray-core/v1/common/protocol"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type DetourConfig struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DetourConfig) Reset() {
|
||||
*x = DetourConfig{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *DetourConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DetourConfig) ProtoMessage() {}
|
||||
|
||||
func (x *DetourConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DetourConfig.ProtoReflect.Descriptor instead.
|
||||
func (*DetourConfig) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DetourConfig) GetTo() string {
|
||||
if x != nil {
|
||||
return x.To
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DefaultConfig struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
AlterId uint32 `protobuf:"varint,1,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DefaultConfig) Reset() {
|
||||
*x = DefaultConfig{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *DefaultConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DefaultConfig) ProtoMessage() {}
|
||||
|
||||
func (x *DefaultConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DefaultConfig.ProtoReflect.Descriptor instead.
|
||||
func (*DefaultConfig) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *DefaultConfig) GetAlterId() uint32 {
|
||||
if x != nil {
|
||||
return x.AlterId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *DefaultConfig) GetLevel() uint32 {
|
||||
if x != nil {
|
||||
return x.Level
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
User []*protocol.User `protobuf:"bytes,1,rep,name=user,proto3" json:"user,omitempty"`
|
||||
Default *DefaultConfig `protobuf:"bytes,2,opt,name=default,proto3" json:"default,omitempty"`
|
||||
Detour *DetourConfig `protobuf:"bytes,3,opt,name=detour,proto3" json:"detour,omitempty"`
|
||||
SecureEncryptionOnly bool `protobuf:"varint,4,opt,name=secure_encryption_only,json=secureEncryptionOnly,proto3" json:"secure_encryption_only,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_vmess_inbound_config_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Config) GetUser() []*protocol.User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetDefault() *DefaultConfig {
|
||||
if x != nil {
|
||||
return x.Default
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetDetour() *DetourConfig {
|
||||
if x != nil {
|
||||
return x.Detour
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetSecureEncryptionOnly() bool {
|
||||
if x != nil {
|
||||
return x.SecureEncryptionOnly
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_proxy_vmess_inbound_config_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proxy_vmess_inbound_config_proto_rawDesc = []byte{
|
||||
0x0a, 0x20, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e,
|
||||
0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x18, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x1a, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x75, 0x73,
|
||||
0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1e, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x6f,
|
||||
0x75, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61,
|
||||
0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0xf1, 0x01, 0x0a, 0x06, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 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,
|
||||
0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e,
|
||||
0x64, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||
0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x3e, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x6f,
|
||||
0x75, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f,
|
||||
0x75, 0x6e, 0x64, 0x2e, 0x44, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x52, 0x06, 0x64, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x65, 0x63, 0x75,
|
||||
0x72, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x6e,
|
||||
0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65,
|
||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x6e, 0x6c, 0x79, 0x42, 0x6d,
|
||||
0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79,
|
||||
0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01,
|
||||
0x5a, 0x30, 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, 0x76, 0x31, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 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, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_proxy_vmess_inbound_config_proto_rawDescOnce sync.Once
|
||||
file_proxy_vmess_inbound_config_proto_rawDescData = file_proxy_vmess_inbound_config_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_proxy_vmess_inbound_config_proto_rawDescGZIP() []byte {
|
||||
file_proxy_vmess_inbound_config_proto_rawDescOnce.Do(func() {
|
||||
file_proxy_vmess_inbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_inbound_config_proto_rawDescData)
|
||||
})
|
||||
return file_proxy_vmess_inbound_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_vmess_inbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_proxy_vmess_inbound_config_proto_goTypes = []interface{}{
|
||||
(*DetourConfig)(nil), // 0: xray.proxy.vmess.inbound.DetourConfig
|
||||
(*DefaultConfig)(nil), // 1: xray.proxy.vmess.inbound.DefaultConfig
|
||||
(*Config)(nil), // 2: xray.proxy.vmess.inbound.Config
|
||||
(*protocol.User)(nil), // 3: xray.common.protocol.User
|
||||
}
|
||||
var file_proxy_vmess_inbound_config_proto_depIdxs = []int32{
|
||||
3, // 0: xray.proxy.vmess.inbound.Config.user:type_name -> xray.common.protocol.User
|
||||
1, // 1: xray.proxy.vmess.inbound.Config.default:type_name -> xray.proxy.vmess.inbound.DefaultConfig
|
||||
0, // 2: xray.proxy.vmess.inbound.Config.detour:type_name -> xray.proxy.vmess.inbound.DetourConfig
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_vmess_inbound_config_proto_init() }
|
||||
func file_proxy_vmess_inbound_config_proto_init() {
|
||||
if File_proxy_vmess_inbound_config_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_proxy_vmess_inbound_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DetourConfig); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proxy_vmess_inbound_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DefaultConfig); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proxy_vmess_inbound_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Config); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proxy_vmess_inbound_config_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_vmess_inbound_config_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_vmess_inbound_config_proto_depIdxs,
|
||||
MessageInfos: file_proxy_vmess_inbound_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_vmess_inbound_config_proto = out.File
|
||||
file_proxy_vmess_inbound_config_proto_rawDesc = nil
|
||||
file_proxy_vmess_inbound_config_proto_goTypes = nil
|
||||
file_proxy_vmess_inbound_config_proto_depIdxs = nil
|
||||
}
|
25
proxy/vmess/inbound/config.proto
Normal file
25
proxy/vmess/inbound/config.proto
Normal file
|
@ -0,0 +1,25 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package xray.proxy.vmess.inbound;
|
||||
option csharp_namespace = "Xray.Proxy.Vmess.Inbound";
|
||||
option go_package = "github.com/xtls/xray-core/v1/proxy/vmess/inbound";
|
||||
option java_package = "com.xray.proxy.vmess.inbound";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protocol/user.proto";
|
||||
|
||||
message DetourConfig {
|
||||
string to = 1;
|
||||
}
|
||||
|
||||
message DefaultConfig {
|
||||
uint32 alter_id = 1;
|
||||
uint32 level = 2;
|
||||
}
|
||||
|
||||
message Config {
|
||||
repeated xray.common.protocol.User user = 1;
|
||||
DefaultConfig default = 2;
|
||||
DetourConfig detour = 3;
|
||||
bool secure_encryption_only = 4;
|
||||
}
|
9
proxy/vmess/inbound/errors.generated.go
Normal file
9
proxy/vmess/inbound/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package inbound
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
357
proxy/vmess/inbound/inbound.go
Normal file
357
proxy/vmess/inbound/inbound.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
// +build !confonly
|
||||
|
||||
package inbound
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/errors"
|
||||
"github.com/xtls/xray-core/v1/common/log"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/session"
|
||||
"github.com/xtls/xray-core/v1/common/signal"
|
||||
"github.com/xtls/xray-core/v1/common/task"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
"github.com/xtls/xray-core/v1/core"
|
||||
feature_inbound "github.com/xtls/xray-core/v1/features/inbound"
|
||||
"github.com/xtls/xray-core/v1/features/policy"
|
||||
"github.com/xtls/xray-core/v1/features/routing"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
)
|
||||
|
||||
type userByEmail struct {
|
||||
sync.Mutex
|
||||
cache map[string]*protocol.MemoryUser
|
||||
defaultLevel uint32
|
||||
defaultAlterIDs uint16
|
||||
}
|
||||
|
||||
func newUserByEmail(config *DefaultConfig) *userByEmail {
|
||||
return &userByEmail{
|
||||
cache: make(map[string]*protocol.MemoryUser),
|
||||
defaultLevel: config.Level,
|
||||
defaultAlterIDs: uint16(config.AlterId),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *userByEmail) addNoLock(u *protocol.MemoryUser) bool {
|
||||
email := strings.ToLower(u.Email)
|
||||
_, found := v.cache[email]
|
||||
if found {
|
||||
return false
|
||||
}
|
||||
v.cache[email] = u
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *userByEmail) Add(u *protocol.MemoryUser) bool {
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
return v.addNoLock(u)
|
||||
}
|
||||
|
||||
func (v *userByEmail) Get(email string) (*protocol.MemoryUser, bool) {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
user, found := v.cache[email]
|
||||
if !found {
|
||||
id := uuid.New()
|
||||
rawAccount := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: uint32(v.defaultAlterIDs),
|
||||
}
|
||||
account, err := rawAccount.AsAccount()
|
||||
common.Must(err)
|
||||
user = &protocol.MemoryUser{
|
||||
Level: v.defaultLevel,
|
||||
Email: email,
|
||||
Account: account,
|
||||
}
|
||||
v.cache[email] = user
|
||||
}
|
||||
return user, found
|
||||
}
|
||||
|
||||
func (v *userByEmail) Remove(email string) bool {
|
||||
email = strings.ToLower(email)
|
||||
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
if _, found := v.cache[email]; !found {
|
||||
return false
|
||||
}
|
||||
delete(v.cache, email)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handler is an inbound connection handler that handles messages in VMess protocol.
|
||||
type Handler struct {
|
||||
policyManager policy.Manager
|
||||
inboundHandlerManager feature_inbound.Manager
|
||||
clients *vmess.TimedUserValidator
|
||||
usersByEmail *userByEmail
|
||||
detours *DetourConfig
|
||||
sessionHistory *encoding.SessionHistory
|
||||
secure bool
|
||||
}
|
||||
|
||||
// New creates a new VMess inbound handler.
|
||||
func New(ctx context.Context, config *Config) (*Handler, error) {
|
||||
v := core.MustFromContext(ctx)
|
||||
handler := &Handler{
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager),
|
||||
clients: vmess.NewTimedUserValidator(protocol.DefaultIDHash),
|
||||
detours: config.Detour,
|
||||
usersByEmail: newUserByEmail(config.GetDefaultValue()),
|
||||
sessionHistory: encoding.NewSessionHistory(),
|
||||
secure: config.SecureEncryptionOnly,
|
||||
}
|
||||
|
||||
for _, user := range config.User {
|
||||
mUser, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, newError("failed to get VMess user").Base(err)
|
||||
}
|
||||
|
||||
if err := handler.AddUser(ctx, mUser); err != nil {
|
||||
return nil, newError("failed to initiate user").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (h *Handler) Close() error {
|
||||
return errors.Combine(
|
||||
h.clients.Close(),
|
||||
h.sessionHistory.Close(),
|
||||
common.Close(h.usersByEmail))
|
||||
}
|
||||
|
||||
// Network implements proxy.Inbound.Network().
|
||||
func (*Handler) Network() []net.Network {
|
||||
return []net.Network{net.Network_TCP, net.Network_UNIX}
|
||||
}
|
||||
|
||||
func (h *Handler) GetUser(email string) *protocol.MemoryUser {
|
||||
user, existing := h.usersByEmail.Get(email)
|
||||
if !existing {
|
||||
h.clients.Add(user)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (h *Handler) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
|
||||
if len(user.Email) > 0 && !h.usersByEmail.Add(user) {
|
||||
return newError("User ", user.Email, " already exists.")
|
||||
}
|
||||
return h.clients.Add(user)
|
||||
}
|
||||
|
||||
func (h *Handler) RemoveUser(ctx context.Context, email string) error {
|
||||
if email == "" {
|
||||
return newError("Email must not be empty.")
|
||||
}
|
||||
if !h.usersByEmail.Remove(email) {
|
||||
return newError("User ", email, " not found.")
|
||||
}
|
||||
h.clients.Remove(email)
|
||||
return nil
|
||||
}
|
||||
|
||||
func transferResponse(timer signal.ActivityUpdater, session *encoding.ServerSession, request *protocol.RequestHeader, response *protocol.ResponseHeader, input buf.Reader, output *buf.BufferedWriter) error {
|
||||
session.EncodeResponseHeader(response, output)
|
||||
|
||||
bodyWriter := session.EncodeResponseBody(request, output)
|
||||
|
||||
{
|
||||
// Optimize for small response packet
|
||||
data, err := input.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bodyWriter.WriteMultiBuffer(data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := output.SetBuffered(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if err := bodyWriter.WriteMultiBuffer(buf.MultiBuffer{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isInsecureEncryption(s protocol.SecurityType) bool {
|
||||
return s == protocol.SecurityType_NONE || s == protocol.SecurityType_LEGACY || s == protocol.SecurityType_UNKNOWN
|
||||
}
|
||||
|
||||
// Process implements proxy.Inbound.Process().
|
||||
func (h *Handler) Process(ctx context.Context, network net.Network, connection internet.Connection, dispatcher routing.Dispatcher) error {
|
||||
sessionPolicy := h.policyManager.ForLevel(0)
|
||||
if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return newError("unable to set read deadline").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
reader := &buf.BufferedReader{Reader: buf.NewReader(connection)}
|
||||
svrSession := encoding.NewServerSession(h.clients, h.sessionHistory)
|
||||
request, err := svrSession.DecodeRequestHeader(reader)
|
||||
if err != nil {
|
||||
if errors.Cause(err) != io.EOF {
|
||||
log.Record(&log.AccessMessage{
|
||||
From: connection.RemoteAddr(),
|
||||
To: "",
|
||||
Status: log.AccessRejected,
|
||||
Reason: err,
|
||||
})
|
||||
err = newError("invalid request from ", connection.RemoteAddr()).Base(err).AtInfo()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if h.secure && isInsecureEncryption(request.Security) {
|
||||
log.Record(&log.AccessMessage{
|
||||
From: connection.RemoteAddr(),
|
||||
To: "",
|
||||
Status: log.AccessRejected,
|
||||
Reason: "Insecure encryption",
|
||||
Email: request.User.Email,
|
||||
})
|
||||
return newError("client is using insecure encryption: ", request.Security)
|
||||
}
|
||||
|
||||
if request.Command != protocol.RequestCommandMux {
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: connection.RemoteAddr(),
|
||||
To: request.Destination(),
|
||||
Status: log.AccessAccepted,
|
||||
Reason: "",
|
||||
Email: request.User.Email,
|
||||
})
|
||||
}
|
||||
|
||||
newError("received request for ", request.Destination()).WriteToLog(session.ExportIDToError(ctx))
|
||||
|
||||
if err := connection.SetReadDeadline(time.Time{}); err != nil {
|
||||
newError("unable to set back read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx))
|
||||
}
|
||||
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
if inbound == nil {
|
||||
panic("no inbound metadata")
|
||||
}
|
||||
inbound.User = request.User
|
||||
|
||||
sessionPolicy = h.policyManager.ForLevel(request.User.Level)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
link, err := dispatcher.Dispatch(ctx, request.Destination())
|
||||
if err != nil {
|
||||
return newError("failed to dispatch request to ", request.Destination()).Base(err)
|
||||
}
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
|
||||
bodyReader := svrSession.DecodeRequestBody(request, reader)
|
||||
if err := buf.Copy(bodyReader, link.Writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return newError("failed to transfer request").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
writer := buf.NewBufferedWriter(buf.NewWriter(connection))
|
||||
defer writer.Flush()
|
||||
|
||||
response := &protocol.ResponseHeader{
|
||||
Command: h.generateCommand(ctx, request),
|
||||
}
|
||||
return transferResponse(timer, svrSession, request, response, link.Reader, writer)
|
||||
}
|
||||
|
||||
var requestDonePost = task.OnSuccess(requestDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDonePost, responseDone); err != nil {
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
return newError("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) generateCommand(ctx context.Context, request *protocol.RequestHeader) protocol.ResponseCommand {
|
||||
if h.detours != nil {
|
||||
tag := h.detours.To
|
||||
if h.inboundHandlerManager != nil {
|
||||
handler, err := h.inboundHandlerManager.GetHandler(ctx, tag)
|
||||
if err != nil {
|
||||
newError("failed to get detour handler: ", tag).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
||||
return nil
|
||||
}
|
||||
proxyHandler, port, availableMin := handler.GetRandomInboundProxy()
|
||||
inboundHandler, ok := proxyHandler.(*Handler)
|
||||
if ok && inboundHandler != nil {
|
||||
if availableMin > 255 {
|
||||
availableMin = 255
|
||||
}
|
||||
|
||||
newError("pick detour handler for port ", port, " for ", availableMin, " minutes.").AtDebug().WriteToLog(session.ExportIDToError(ctx))
|
||||
user := inboundHandler.GetUser(request.User.Email)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
account := user.Account.(*vmess.MemoryAccount)
|
||||
return &protocol.CommandSwitchAccount{
|
||||
Port: port,
|
||||
ID: account.ID.UUID(),
|
||||
AlterIds: uint16(len(account.AlterIDs)),
|
||||
Level: user.Level,
|
||||
ValidMin: byte(availableMin),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
}
|
44
proxy/vmess/outbound/command.go
Normal file
44
proxy/vmess/outbound/command.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// +build !confonly
|
||||
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
)
|
||||
|
||||
func (h *Handler) handleSwitchAccount(cmd *protocol.CommandSwitchAccount) {
|
||||
rawAccount := &vmess.Account{
|
||||
Id: cmd.ID.String(),
|
||||
AlterId: uint32(cmd.AlterIds),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_LEGACY,
|
||||
},
|
||||
}
|
||||
|
||||
account, err := rawAccount.AsAccount()
|
||||
common.Must(err)
|
||||
user := &protocol.MemoryUser{
|
||||
Email: "",
|
||||
Level: cmd.Level,
|
||||
Account: account,
|
||||
}
|
||||
dest := net.TCPDestination(cmd.Host, cmd.Port)
|
||||
until := time.Now().Add(time.Duration(cmd.ValidMin) * time.Minute)
|
||||
h.serverList.AddServer(protocol.NewServerSpec(dest, protocol.BeforeTime(until), user))
|
||||
}
|
||||
|
||||
func (h *Handler) handleCommand(dest net.Destination, cmd protocol.ResponseCommand) {
|
||||
switch typedCommand := cmd.(type) {
|
||||
case *protocol.CommandSwitchAccount:
|
||||
if typedCommand.Host == nil {
|
||||
typedCommand.Host = dest.Address
|
||||
}
|
||||
h.handleSwitchAccount(typedCommand)
|
||||
default:
|
||||
}
|
||||
}
|
1
proxy/vmess/outbound/config.go
Normal file
1
proxy/vmess/outbound/config.go
Normal file
|
@ -0,0 +1 @@
|
|||
package outbound
|
163
proxy/vmess/outbound/config.pb.go
Normal file
163
proxy/vmess/outbound/config.pb.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.14.0
|
||||
// source: proxy/vmess/outbound/config.proto
|
||||
|
||||
package outbound
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
protocol "github.com/xtls/xray-core/v1/common/protocol"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Receiver []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=Receiver,proto3" json:"Receiver,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proxy_vmess_outbound_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_vmess_outbound_config_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_vmess_outbound_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetReceiver() []*protocol.ServerEndpoint {
|
||||
if x != nil {
|
||||
return x.Receiver
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_vmess_outbound_config_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proxy_vmess_outbound_config_proto_rawDesc = []byte{
|
||||
0x0a, 0x21, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75,
|
||||
0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x12, 0x19, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e,
|
||||
0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x21,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x22, 0x4a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x08, 0x52,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e,
|
||||
0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f,
|
||||
0x69, 0x6e, 0x74, 0x52, 0x08, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x70, 0x0a,
|
||||
0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e,
|
||||
0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01,
|
||||
0x5a, 0x31, 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, 0x76, 0x31, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75, 0x74, 0x62, 0x6f,
|
||||
0x75, 0x6e, 0x64, 0xaa, 0x02, 0x19, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79,
|
||||
0x2e, 0x56, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_proxy_vmess_outbound_config_proto_rawDescOnce sync.Once
|
||||
file_proxy_vmess_outbound_config_proto_rawDescData = file_proxy_vmess_outbound_config_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_proxy_vmess_outbound_config_proto_rawDescGZIP() []byte {
|
||||
file_proxy_vmess_outbound_config_proto_rawDescOnce.Do(func() {
|
||||
file_proxy_vmess_outbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_outbound_config_proto_rawDescData)
|
||||
})
|
||||
return file_proxy_vmess_outbound_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_vmess_outbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_proxy_vmess_outbound_config_proto_goTypes = []interface{}{
|
||||
(*Config)(nil), // 0: xray.proxy.vmess.outbound.Config
|
||||
(*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint
|
||||
}
|
||||
var file_proxy_vmess_outbound_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.proxy.vmess.outbound.Config.Receiver:type_name -> xray.common.protocol.ServerEndpoint
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_vmess_outbound_config_proto_init() }
|
||||
func file_proxy_vmess_outbound_config_proto_init() {
|
||||
if File_proxy_vmess_outbound_config_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_proxy_vmess_outbound_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Config); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proxy_vmess_outbound_config_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_proxy_vmess_outbound_config_proto_goTypes,
|
||||
DependencyIndexes: file_proxy_vmess_outbound_config_proto_depIdxs,
|
||||
MessageInfos: file_proxy_vmess_outbound_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proxy_vmess_outbound_config_proto = out.File
|
||||
file_proxy_vmess_outbound_config_proto_rawDesc = nil
|
||||
file_proxy_vmess_outbound_config_proto_goTypes = nil
|
||||
file_proxy_vmess_outbound_config_proto_depIdxs = nil
|
||||
}
|
13
proxy/vmess/outbound/config.proto
Normal file
13
proxy/vmess/outbound/config.proto
Normal file
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package xray.proxy.vmess.outbound;
|
||||
option csharp_namespace = "Xray.Proxy.Vmess.Outbound";
|
||||
option go_package = "github.com/xtls/xray-core/v1/proxy/vmess/outbound";
|
||||
option java_package = "com.xray.proxy.vmess.outbound";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protocol/server_spec.proto";
|
||||
|
||||
message Config {
|
||||
repeated xray.common.protocol.ServerEndpoint Receiver = 1;
|
||||
}
|
9
proxy/vmess/outbound/errors.generated.go
Normal file
9
proxy/vmess/outbound/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package outbound
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
205
proxy/vmess/outbound/outbound.go
Normal file
205
proxy/vmess/outbound/outbound.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
// +build !confonly
|
||||
|
||||
package outbound
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/platform"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/retry"
|
||||
"github.com/xtls/xray-core/v1/common/session"
|
||||
"github.com/xtls/xray-core/v1/common/signal"
|
||||
"github.com/xtls/xray-core/v1/common/task"
|
||||
core "github.com/xtls/xray-core/v1/core"
|
||||
"github.com/xtls/xray-core/v1/features/policy"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
"github.com/xtls/xray-core/v1/transport"
|
||||
"github.com/xtls/xray-core/v1/transport/internet"
|
||||
)
|
||||
|
||||
// Handler is an outbound connection handler for VMess protocol.
|
||||
type Handler struct {
|
||||
serverList *protocol.ServerList
|
||||
serverPicker protocol.ServerPicker
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
// New creates a new VMess outbound handler.
|
||||
func New(ctx context.Context, config *Config) (*Handler, error) {
|
||||
serverList := protocol.NewServerList()
|
||||
for _, rec := range config.Receiver {
|
||||
s, err := protocol.NewServerSpecFromPB(rec)
|
||||
if err != nil {
|
||||
return nil, newError("failed to parse server spec").Base(err)
|
||||
}
|
||||
serverList.AddServer(s)
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
handler := &Handler{
|
||||
serverList: serverList,
|
||||
serverPicker: protocol.NewRoundRobinServerPicker(serverList),
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
// Process implements proxy.Outbound.Process().
|
||||
func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
|
||||
var rec *protocol.ServerSpec
|
||||
var conn internet.Connection
|
||||
|
||||
err := retry.ExponentialBackoff(5, 200).On(func() error {
|
||||
rec = h.serverPicker.PickServer()
|
||||
rawConn, err := dialer.Dial(ctx, rec.Destination())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn = rawConn
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return newError("failed to find an available destination").Base(err).AtWarning()
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
outbound := session.OutboundFromContext(ctx)
|
||||
if outbound == nil || !outbound.Target.IsValid() {
|
||||
return newError("target not specified").AtError()
|
||||
}
|
||||
|
||||
target := outbound.Target
|
||||
newError("tunneling request to ", target, " via ", rec.Destination()).WriteToLog(session.ExportIDToError(ctx))
|
||||
|
||||
command := protocol.RequestCommandTCP
|
||||
if target.Network == net.Network_UDP {
|
||||
command = protocol.RequestCommandUDP
|
||||
}
|
||||
if target.Address.Family().IsDomain() && target.Address.Domain() == "v1.mux.cool" {
|
||||
command = protocol.RequestCommandMux
|
||||
}
|
||||
|
||||
user := rec.PickUser()
|
||||
request := &protocol.RequestHeader{
|
||||
Version: encoding.Version,
|
||||
User: user,
|
||||
Command: command,
|
||||
Address: target.Address,
|
||||
Port: target.Port,
|
||||
Option: protocol.RequestOptionChunkStream,
|
||||
}
|
||||
|
||||
account := request.User.Account.(*vmess.MemoryAccount)
|
||||
request.Security = account.Security
|
||||
|
||||
if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 {
|
||||
request.Option.Set(protocol.RequestOptionChunkMasking)
|
||||
}
|
||||
|
||||
if shouldEnablePadding(request.Security) && request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
request.Option.Set(protocol.RequestOptionGlobalPadding)
|
||||
}
|
||||
|
||||
input := link.Reader
|
||||
output := link.Writer
|
||||
|
||||
isAEAD := false
|
||||
if !aeadDisabled && len(account.AlterIDs) == 0 {
|
||||
isAEAD = true
|
||||
}
|
||||
|
||||
session := encoding.NewClientSession(ctx, isAEAD, protocol.DefaultIDHash)
|
||||
sessionPolicy := h.policyManager.ForLevel(request.User.Level)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
|
||||
writer := buf.NewBufferedWriter(buf.NewWriter(conn))
|
||||
if err := session.EncodeRequestHeader(request, writer); err != nil {
|
||||
return newError("failed to encode request").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
bodyWriter := session.EncodeRequestBody(request, writer)
|
||||
if err := buf.CopyOnceTimeout(input, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return newError("failed to write first payload").Base(err)
|
||||
}
|
||||
|
||||
if err := writer.SetBuffered(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if err := bodyWriter.WriteMultiBuffer(buf.MultiBuffer{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
reader := &buf.BufferedReader{Reader: buf.NewReader(conn)}
|
||||
header, err := session.DecodeResponseHeader(reader)
|
||||
if err != nil {
|
||||
return newError("failed to read header").Base(err)
|
||||
}
|
||||
h.handleCommand(rec.Destination(), header.Command)
|
||||
|
||||
bodyReader := session.DecodeResponseBody(request, reader)
|
||||
|
||||
return buf.Copy(bodyReader, output, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
var responseDonePost = task.OnSuccess(responseDone, task.Close(output))
|
||||
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
|
||||
return newError("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
enablePadding = false
|
||||
aeadDisabled = false
|
||||
)
|
||||
|
||||
func shouldEnablePadding(s protocol.SecurityType) bool {
|
||||
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
|
||||
paddingValue := platform.NewEnvFlag("xray.vmess.padding").GetValue(func() string { return defaultFlagValue })
|
||||
if paddingValue != defaultFlagValue {
|
||||
enablePadding = true
|
||||
}
|
||||
|
||||
isAeadDisabled := platform.NewEnvFlag("xray.vmess.aead.disabled").GetValue(func() string { return defaultFlagValue })
|
||||
if isAeadDisabled == "true" {
|
||||
aeadDisabled = true
|
||||
}
|
||||
}
|
252
proxy/vmess/validator.go
Normal file
252
proxy/vmess/validator.go
Normal file
|
@ -0,0 +1,252 @@
|
|||
// +build !confonly
|
||||
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash/crc64"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/common/task"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess/aead"
|
||||
)
|
||||
|
||||
const (
|
||||
updateInterval = 10 * time.Second
|
||||
cacheDurationSec = 120
|
||||
)
|
||||
|
||||
type user struct {
|
||||
user protocol.MemoryUser
|
||||
lastSec protocol.Timestamp
|
||||
}
|
||||
|
||||
// TimedUserValidator is a user Validator based on time.
|
||||
type TimedUserValidator struct {
|
||||
sync.RWMutex
|
||||
users []*user
|
||||
userHash map[[16]byte]indexTimePair
|
||||
hasher protocol.IDHash
|
||||
baseTime protocol.Timestamp
|
||||
task *task.Periodic
|
||||
|
||||
behaviorSeed uint64
|
||||
behaviorFused bool
|
||||
|
||||
aeadDecoderHolder *aead.AuthIDDecoderHolder
|
||||
}
|
||||
|
||||
type indexTimePair struct {
|
||||
user *user
|
||||
timeInc uint32
|
||||
|
||||
taintedFuse *uint32
|
||||
}
|
||||
|
||||
// NewTimedUserValidator creates a new TimedUserValidator.
|
||||
func NewTimedUserValidator(hasher protocol.IDHash) *TimedUserValidator {
|
||||
tuv := &TimedUserValidator{
|
||||
users: make([]*user, 0, 16),
|
||||
userHash: make(map[[16]byte]indexTimePair, 1024),
|
||||
hasher: hasher,
|
||||
baseTime: protocol.Timestamp(time.Now().Unix() - cacheDurationSec*2),
|
||||
aeadDecoderHolder: aead.NewAuthIDDecoderHolder(),
|
||||
}
|
||||
tuv.task = &task.Periodic{
|
||||
Interval: updateInterval,
|
||||
Execute: func() error {
|
||||
tuv.updateUserHash()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
common.Must(tuv.task.Start())
|
||||
return tuv
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) generateNewHashes(nowSec protocol.Timestamp, user *user) {
|
||||
var hashValue [16]byte
|
||||
genEndSec := nowSec + cacheDurationSec
|
||||
genHashForID := func(id *protocol.ID) {
|
||||
idHash := v.hasher(id.Bytes())
|
||||
genBeginSec := user.lastSec
|
||||
if genBeginSec < nowSec-cacheDurationSec {
|
||||
genBeginSec = nowSec - cacheDurationSec
|
||||
}
|
||||
for ts := genBeginSec; ts <= genEndSec; ts++ {
|
||||
common.Must2(serial.WriteUint64(idHash, uint64(ts)))
|
||||
idHash.Sum(hashValue[:0])
|
||||
idHash.Reset()
|
||||
|
||||
v.userHash[hashValue] = indexTimePair{
|
||||
user: user,
|
||||
timeInc: uint32(ts - v.baseTime),
|
||||
taintedFuse: new(uint32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account := user.user.Account.(*MemoryAccount)
|
||||
|
||||
genHashForID(account.ID)
|
||||
for _, id := range account.AlterIDs {
|
||||
genHashForID(id)
|
||||
}
|
||||
user.lastSec = genEndSec
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) removeExpiredHashes(expire uint32) {
|
||||
for key, pair := range v.userHash {
|
||||
if pair.timeInc < expire {
|
||||
delete(v.userHash, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) updateUserHash() {
|
||||
now := time.Now()
|
||||
nowSec := protocol.Timestamp(now.Unix())
|
||||
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
for _, user := range v.users {
|
||||
v.generateNewHashes(nowSec, user)
|
||||
}
|
||||
|
||||
expire := protocol.Timestamp(now.Unix() - cacheDurationSec)
|
||||
if expire > v.baseTime {
|
||||
v.removeExpiredHashes(uint32(expire - v.baseTime))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) Add(u *protocol.MemoryUser) error {
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
nowSec := time.Now().Unix()
|
||||
|
||||
uu := &user{
|
||||
user: *u,
|
||||
lastSec: protocol.Timestamp(nowSec - cacheDurationSec),
|
||||
}
|
||||
v.users = append(v.users, uu)
|
||||
v.generateNewHashes(protocol.Timestamp(nowSec), uu)
|
||||
|
||||
account := uu.user.Account.(*MemoryAccount)
|
||||
if !v.behaviorFused {
|
||||
hashkdf := hmac.New(sha256.New, []byte("VMESSBSKDF"))
|
||||
hashkdf.Write(account.ID.Bytes())
|
||||
v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))
|
||||
}
|
||||
|
||||
var cmdkeyfl [16]byte
|
||||
copy(cmdkeyfl[:], account.ID.CmdKey())
|
||||
v.aeadDecoderHolder.AddUser(cmdkeyfl, u)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) Get(userHash []byte) (*protocol.MemoryUser, protocol.Timestamp, bool, error) {
|
||||
v.RLock()
|
||||
defer v.RUnlock()
|
||||
|
||||
v.behaviorFused = true
|
||||
|
||||
var fixedSizeHash [16]byte
|
||||
copy(fixedSizeHash[:], userHash)
|
||||
pair, found := v.userHash[fixedSizeHash]
|
||||
if found {
|
||||
user := pair.user.user
|
||||
if atomic.LoadUint32(pair.taintedFuse) == 0 {
|
||||
return &user, protocol.Timestamp(pair.timeInc) + v.baseTime, true, nil
|
||||
}
|
||||
return nil, 0, false, ErrTainted
|
||||
}
|
||||
return nil, 0, false, ErrNotFound
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) GetAEAD(userHash []byte) (*protocol.MemoryUser, bool, error) {
|
||||
v.RLock()
|
||||
defer v.RUnlock()
|
||||
|
||||
var userHashFL [16]byte
|
||||
copy(userHashFL[:], userHash)
|
||||
|
||||
userd, err := v.aeadDecoderHolder.Match(userHashFL)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return userd.(*protocol.MemoryUser), true, err
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) Remove(email string) bool {
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
email = strings.ToLower(email)
|
||||
idx := -1
|
||||
for i, u := range v.users {
|
||||
if strings.EqualFold(u.user.Email, email) {
|
||||
idx = i
|
||||
var cmdkeyfl [16]byte
|
||||
copy(cmdkeyfl[:], u.user.Account.(*MemoryAccount).ID.CmdKey())
|
||||
v.aeadDecoderHolder.RemoveUser(cmdkeyfl)
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
ulen := len(v.users)
|
||||
|
||||
v.users[idx] = v.users[ulen-1]
|
||||
v.users[ulen-1] = nil
|
||||
v.users = v.users[:ulen-1]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (v *TimedUserValidator) Close() error {
|
||||
return v.task.Close()
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) GetBehaviorSeed() uint64 {
|
||||
v.Lock()
|
||||
defer v.Unlock()
|
||||
|
||||
v.behaviorFused = true
|
||||
if v.behaviorSeed == 0 {
|
||||
v.behaviorSeed = dice.RollUint64()
|
||||
}
|
||||
return v.behaviorSeed
|
||||
}
|
||||
|
||||
func (v *TimedUserValidator) BurnTaintFuse(userHash []byte) error {
|
||||
v.RLock()
|
||||
defer v.RUnlock()
|
||||
|
||||
var userHashFL [16]byte
|
||||
copy(userHashFL[:], userHash)
|
||||
|
||||
pair, found := v.userHash[userHashFL]
|
||||
if found {
|
||||
if atomic.CompareAndSwapUint32(pair.taintedFuse, 0, 1) {
|
||||
return nil
|
||||
}
|
||||
return ErrTainted
|
||||
}
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
var ErrNotFound = newError("Not Found")
|
||||
|
||||
var ErrTainted = newError("ErrTainted")
|
110
proxy/vmess/validator_test.go
Normal file
110
proxy/vmess/validator_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package vmess_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
)
|
||||
|
||||
func toAccount(a *Account) protocol.Account {
|
||||
account, err := a.AsAccount()
|
||||
common.Must(err)
|
||||
return account
|
||||
}
|
||||
|
||||
func TestUserValidator(t *testing.T) {
|
||||
hasher := protocol.DefaultIDHash
|
||||
v := NewTimedUserValidator(hasher)
|
||||
defer common.Close(v)
|
||||
|
||||
id := uuid.New()
|
||||
user := &protocol.MemoryUser{
|
||||
Email: "test",
|
||||
Account: toAccount(&Account{
|
||||
Id: id.String(),
|
||||
AlterId: 8,
|
||||
}),
|
||||
}
|
||||
common.Must(v.Add(user))
|
||||
|
||||
{
|
||||
testSmallLag := func(lag time.Duration) {
|
||||
ts := protocol.Timestamp(time.Now().Add(time.Second * lag).Unix())
|
||||
idHash := hasher(id.Bytes())
|
||||
common.Must2(serial.WriteUint64(idHash, uint64(ts)))
|
||||
userHash := idHash.Sum(nil)
|
||||
|
||||
euser, ets, found, _ := v.Get(userHash)
|
||||
if !found {
|
||||
t.Fatal("user not found")
|
||||
}
|
||||
if euser.Email != user.Email {
|
||||
t.Error("unexpected user email: ", euser.Email, " want ", user.Email)
|
||||
}
|
||||
if ets != ts {
|
||||
t.Error("unexpected timestamp: ", ets, " want ", ts)
|
||||
}
|
||||
}
|
||||
|
||||
testSmallLag(0)
|
||||
testSmallLag(40)
|
||||
testSmallLag(-40)
|
||||
testSmallLag(80)
|
||||
testSmallLag(-80)
|
||||
testSmallLag(120)
|
||||
testSmallLag(-120)
|
||||
}
|
||||
|
||||
{
|
||||
testBigLag := func(lag time.Duration) {
|
||||
ts := protocol.Timestamp(time.Now().Add(time.Second * lag).Unix())
|
||||
idHash := hasher(id.Bytes())
|
||||
common.Must2(serial.WriteUint64(idHash, uint64(ts)))
|
||||
userHash := idHash.Sum(nil)
|
||||
|
||||
euser, _, found, _ := v.Get(userHash)
|
||||
if found || euser != nil {
|
||||
t.Error("unexpected user")
|
||||
}
|
||||
}
|
||||
|
||||
testBigLag(121)
|
||||
testBigLag(-121)
|
||||
testBigLag(310)
|
||||
testBigLag(-310)
|
||||
testBigLag(500)
|
||||
testBigLag(-500)
|
||||
}
|
||||
|
||||
if v := v.Remove(user.Email); !v {
|
||||
t.Error("unable to remove user")
|
||||
}
|
||||
if v := v.Remove(user.Email); v {
|
||||
t.Error("remove user twice")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUserValidator(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
hasher := protocol.DefaultIDHash
|
||||
v := NewTimedUserValidator(hasher)
|
||||
|
||||
for j := 0; j < 1500; j++ {
|
||||
id := uuid.New()
|
||||
v.Add(&protocol.MemoryUser{
|
||||
Email: "test",
|
||||
Account: toAccount(&Account{
|
||||
Id: id.String(),
|
||||
AlterId: 16,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
common.Close(v)
|
||||
}
|
||||
}
|
8
proxy/vmess/vmess.go
Normal file
8
proxy/vmess/vmess.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Package vmess contains the implementation of VMess protocol and transportation.
|
||||
//
|
||||
// VMess contains both inbound and outbound connections. VMess inbound is usually used on servers
|
||||
// together with 'freedom' to talk to final destination, while VMess outbound is usually used on
|
||||
// clients with 'socks' for proxying.
|
||||
package vmess
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
4
proxy/vmess/vmessCtxInterface.go
Normal file
4
proxy/vmess/vmessCtxInterface.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package vmess
|
||||
|
||||
// example
|
||||
const AlterID = "VMessCtxInterface_AlterID"
|
Loading…
Add table
Add a link
Reference in a new issue