diff --git a/proxy/vmess/config.go b/proxy/vmess/config.go index e1026bfc0..ead9510dc 100644 --- a/proxy/vmess/config.go +++ b/proxy/vmess/config.go @@ -3,6 +3,7 @@ package vmess import ( "encoding/json" "net" + "strings" "github.com/v2ray/v2ray-core/common/log" v2net "github.com/v2ray/v2ray-core/common/net" @@ -40,6 +41,10 @@ type VNextConfig struct { Network string `json:"network"` } +func (config VNextConfig) HasNetwork(network string) bool { + return strings.Contains(config.Network, network) +} + func (config VNextConfig) ToVNextServer() VNextServer { users := make([]user.User, 0, len(config.Users)) for _, user := range config.Users { diff --git a/proxy/vmess/protocol/udp.go b/proxy/vmess/protocol/udp.go deleted file mode 100644 index e4e5d6787..000000000 --- a/proxy/vmess/protocol/udp.go +++ /dev/null @@ -1,140 +0,0 @@ -package protocol - -import ( - "crypto/aes" - "crypto/cipher" - "encoding/binary" - "hash/fnv" - "time" - - "github.com/v2ray/v2ray-core/common/errors" - "github.com/v2ray/v2ray-core/common/log" - v2net "github.com/v2ray/v2ray-core/common/net" - "github.com/v2ray/v2ray-core/proxy/vmess/protocol/user" -) - -type VMessUDP struct { - user user.ID - version byte - address v2net.Address - data []byte -} - -func (message *VMessUDP) ToPacket() v2net.Packet { - dest := v2net.NewUDPDestination(message.address) - return v2net.NewPacket(dest, message.data, false) -} - -func ReadVMessUDP(buffer []byte, userset user.UserSet) (*VMessUDP, error) { - userHash := buffer[:user.IDBytesLen] - userId, timeSec, valid := userset.GetUser(userHash) - if !valid { - return nil, errors.NewAuthenticationError(userHash) - } - - buffer = buffer[user.IDBytesLen:] - aesCipher, err := aes.NewCipher(userId.CmdKey()) - if err != nil { - return nil, err - } - aesStream := cipher.NewCFBDecrypter(aesCipher, user.Int64Hash(timeSec)) - aesStream.XORKeyStream(buffer, buffer) - - fnvHash := binary.BigEndian.Uint32(buffer[:4]) - fnv1a := fnv.New32a() - fnv1a.Write(buffer[4:]) - fnvHashActual := fnv1a.Sum32() - - if fnvHash != fnvHashActual { - log.Warning("Unexpected fhv hash %d, should be %d", fnvHashActual, fnvHash) - return nil, errors.NewCorruptedPacketError() - } - - buffer = buffer[4:] - - vmess := &VMessUDP{ - user: *userId, - version: buffer[0], - } - - // buffer[1] is reserved - - port := binary.BigEndian.Uint16(buffer[2:4]) - addrType := buffer[4] - var address v2net.Address - switch addrType { - case addrTypeIPv4: - address = v2net.IPAddress(buffer[5:9], port) - buffer = buffer[9:] - case addrTypeIPv6: - address = v2net.IPAddress(buffer[5:21], port) - buffer = buffer[21:] - case addrTypeDomain: - domainLength := buffer[5] - domain := string(buffer[6 : 6+domainLength]) - address = v2net.DomainAddress(domain, port) - buffer = buffer[6+domainLength:] - default: - log.Warning("Unexpected address type %d", addrType) - return nil, errors.NewCorruptedPacketError() - } - - vmess.address = address - vmess.data = buffer - - return vmess, nil -} - -func (vmess *VMessUDP) ToBytes(idHash user.CounterHash, randomRangeInt64 user.RandomInt64InRange, buffer []byte) []byte { - if buffer == nil { - buffer = make([]byte, 0, 2*1024) - } - - counter := randomRangeInt64(time.Now().UTC().Unix(), 30) - hash := idHash.Hash(vmess.user.Bytes[:], counter) - - buffer = append(buffer, hash...) - encryptBegin := 16 - - // Placeholder for fnv1a hash - buffer = append(buffer, byte(0), byte(0), byte(0), byte(0)) - fnvHash := 16 - fnvHashBegin := 20 - - buffer = append(buffer, vmess.version) - buffer = append(buffer, byte(0x00)) - buffer = append(buffer, vmess.address.PortBytes()...) - switch { - case vmess.address.IsIPv4(): - buffer = append(buffer, addrTypeIPv4) - buffer = append(buffer, vmess.address.IP()...) - case vmess.address.IsIPv6(): - buffer = append(buffer, addrTypeIPv6) - buffer = append(buffer, vmess.address.IP()...) - case vmess.address.IsDomain(): - buffer = append(buffer, addrTypeDomain) - buffer = append(buffer, byte(len(vmess.address.Domain()))) - buffer = append(buffer, []byte(vmess.address.Domain())...) - } - - buffer = append(buffer, vmess.data...) - - fnv1a := fnv.New32a() - fnv1a.Write(buffer[fnvHashBegin:]) - fnvHashValue := fnv1a.Sum32() - - buffer[fnvHash] = byte(fnvHashValue >> 24) - buffer[fnvHash+1] = byte(fnvHashValue >> 16) - buffer[fnvHash+2] = byte(fnvHashValue >> 8) - buffer[fnvHash+3] = byte(fnvHashValue) - - aesCipher, err := aes.NewCipher(vmess.user.CmdKey()) - if err != nil { - log.Error("VMess failed to create AES cipher: %v", err) - return nil - } - aesStream := cipher.NewCFBEncrypter(aesCipher, user.Int64Hash(counter)) - aesStream.XORKeyStream(buffer[encryptBegin:], buffer[encryptBegin:]) - - return buffer -} diff --git a/proxy/vmess/protocol/udp_test.go b/proxy/vmess/protocol/udp_test.go deleted file mode 100644 index 6cb9cec6d..000000000 --- a/proxy/vmess/protocol/udp_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package protocol - -import ( - "testing" - - v2net "github.com/v2ray/v2ray-core/common/net" - "github.com/v2ray/v2ray-core/proxy/vmess/protocol/user" - "github.com/v2ray/v2ray-core/testing/mocks" - "github.com/v2ray/v2ray-core/testing/unit" -) - -func TestVMessUDPReadWrite(t *testing.T) { - assert := unit.Assert(t) - - userId, err := user.NewID("2b2966ac-16aa-4fbf-8d81-c5f172a3da51") - assert.Error(err).IsNil() - - userSet := mocks.MockUserSet{[]user.ID{}, make(map[string]int), make(map[string]int64)} - userSet.AddUser(user.User{userId}) - - message := &VMessUDP{ - user: userId, - version: byte(0x01), - address: v2net.DomainAddress("v2ray.com", 8372), - data: []byte("An UDP message."), - } - - mockTime := int64(1823730) - buffer := message.ToBytes(user.NewTimeHash(user.HMACHash{}), func(base int64, delta int) int64 { return mockTime }, nil) - - userSet.UserHashes[string(buffer[:16])] = 0 - userSet.Timestamps[string(buffer[:16])] = mockTime - - messageRestored, err := ReadVMessUDP(buffer, &userSet) - assert.Error(err).IsNil() - - assert.String(messageRestored.user.String).Equals(message.user.String) - assert.Byte(messageRestored.version).Equals(message.version) - assert.String(messageRestored.address.String()).Equals(message.address.String()) - assert.Bytes(messageRestored.data).Equals(message.data) -} diff --git a/proxy/vmess/vmessin_udp.go b/proxy/vmess/vmessin_udp.go index 48061931c..d6961b0bd 100644 --- a/proxy/vmess/vmessin_udp.go +++ b/proxy/vmess/vmessin_udp.go @@ -1,8 +1,11 @@ package vmess import ( + "bytes" + "crypto/md5" "net" + v2io "github.com/v2ray/v2ray-core/common/io" "github.com/v2ray/v2ray-core/common/log" v2net "github.com/v2ray/v2ray-core/common/net" "github.com/v2ray/v2ray-core/proxy/vmess/protocol" @@ -36,22 +39,59 @@ func (handler *VMessInboundHandler) AcceptPackets(conn *net.UDPConn) error { log.Error("VMessIn failed to read UDP packets: %v", err) return err } - request, err := protocol.ReadVMessUDP(buffer[:nBytes], handler.clients) + + reader := bytes.NewReader(buffer[:nBytes]) + requestReader := protocol.NewVMessRequestReader(handler.clients) + + request, err := requestReader.Read(reader) if err != nil { - log.Error("VMessIn failed to parse UDP request: %v", err) + log.Warning("VMessIn: Invalid request from (%s): %v", addr.String(), err) return err } - udpPacket := request.ToPacket() - go handler.handlePacket(conn, udpPacket, addr) + cryptReader, err := v2io.NewAesDecryptReader(request.RequestKey[:], request.RequestIV[:], reader) + if err != nil { + log.Error("VMessIn: Failed to create decrypt reader: %v", err) + return err + } + + data := make([]byte, bufferSize) + nBytes, err = cryptReader.Read(data) + if err != nil { + log.Warning("VMessIn: Unable to decrypt data: %v", err) + return err + } + + packet := v2net.NewPacket(request.Destination(), data, false) + go handler.handlePacket(conn, request, packet, addr) } } -func (handler *VMessInboundHandler) handlePacket(conn *net.UDPConn, packet v2net.Packet, clientAddr *net.UDPAddr) { +func (handler *VMessInboundHandler) handlePacket(conn *net.UDPConn, request *protocol.VMessRequest, packet v2net.Packet, clientAddr *net.UDPAddr) { ray := handler.vPoint.DispatchToOutbound(packet) close(ray.InboundInput()) + responseKey := md5.Sum(request.RequestKey[:]) + responseIV := md5.Sum(request.RequestIV[:]) + + buffer := bytes.NewBuffer(make([]byte, 0, bufferSize)) + + response := protocol.NewVMessResponse(request) + responseWriter, err := v2io.NewAesEncryptWriter(responseKey[:], responseIV[:], buffer) + if err != nil { + log.Error("VMessIn: Failed to create encrypt writer: %v", err) + return + } + responseWriter.Write(response[:]) + + hasData := false + if data, ok := <-ray.InboundOutput(); ok { - conn.WriteToUDP(data, clientAddr) + hasData = true + responseWriter.Write(data) + } + + if hasData { + conn.WriteToUDP(buffer.Bytes(), clientAddr) } } diff --git a/proxy/vmess/vmessout.go b/proxy/vmess/vmessout.go index 2933c6276..8bc0afcd9 100644 --- a/proxy/vmess/vmessout.go +++ b/proxy/vmess/vmessout.go @@ -27,12 +27,13 @@ type VNextServer struct { } type VMessOutboundHandler struct { - vPoint *core.Point - packet v2net.Packet - vNextList []VNextServer + vPoint *core.Point + packet v2net.Packet + vNextList []VNextServer + vNextListUDP []VNextServer } -func NewVMessOutboundHandler(vp *core.Point, vNextList []VNextServer, firstPacket v2net.Packet) *VMessOutboundHandler { +func NewVMessOutboundHandler(vp *core.Point, vNextList, vNextListUDP []VNextServer, firstPacket v2net.Packet) *VMessOutboundHandler { return &VMessOutboundHandler{ vPoint: vp, packet: firstPacket, @@ -40,8 +41,8 @@ func NewVMessOutboundHandler(vp *core.Point, vNextList []VNextServer, firstPacke } } -func (handler *VMessOutboundHandler) pickVNext() (v2net.Destination, user.User) { - vNextLen := len(handler.vNextList) +func pickVNext(serverList []VNextServer) (v2net.Destination, user.User) { + vNextLen := len(serverList) if vNextLen == 0 { panic("VMessOut: Zero vNext is configured.") } @@ -50,7 +51,7 @@ func (handler *VMessOutboundHandler) pickVNext() (v2net.Destination, user.User) vNextIndex = mrand.Intn(vNextLen) } - vNext := handler.vNextList[vNextIndex] + vNext := serverList[vNextIndex] vNextUserLen := len(vNext.Users) if vNextUserLen == 0 { panic("VMessOut: Zero User account.") @@ -64,7 +65,7 @@ func (handler *VMessOutboundHandler) pickVNext() (v2net.Destination, user.User) } func (handler *VMessOutboundHandler) Start(ray core.OutboundRay) error { - vNextAddress, vNextUser := handler.pickVNext() + vNextAddress, vNextUser := pickVNext(handler.vNextList) command := protocol.CmdTCP if handler.packet.Destination().IsUDP() { @@ -180,7 +181,8 @@ func handleResponse(conn *net.TCPConn, request *protocol.VMessRequest, output ch } type VMessOutboundHandlerFactory struct { - servers []VNextServer + servers []VNextServer + udpServers []VNextServer } func (factory *VMessOutboundHandlerFactory) Initialize(rawConfig []byte) error { @@ -190,15 +192,22 @@ func (factory *VMessOutboundHandlerFactory) Initialize(rawConfig []byte) error { return err } servers := make([]VNextServer, 0, len(config.VNextList)) + udpServers := make([]VNextServer, 0, len(config.VNextList)) for _, server := range config.VNextList { - servers = append(servers, server.ToVNextServer()) + if server.HasNetwork("tcp") { + servers = append(servers, server.ToVNextServer()) + } + if server.HasNetwork("udp") { + udpServers = append(udpServers, server.ToVNextServer()) + } } factory.servers = servers + factory.udpServers = udpServers return nil } func (factory *VMessOutboundHandlerFactory) Create(vp *core.Point, firstPacket v2net.Packet) (core.OutboundConnectionHandler, error) { - return NewVMessOutboundHandler(vp, factory.servers, firstPacket), nil + return NewVMessOutboundHandler(vp, factory.servers, factory.udpServers, firstPacket), nil } func init() { diff --git a/spec/vmess.md b/spec/vmess.md index 2b9beada1..243480400 100644 --- a/spec/vmess.md +++ b/spec/vmess.md @@ -2,7 +2,7 @@ ## 摘要 * 版本:1 -## TCP +## 格式 ### 数据请求 认证部分: * 16 字节:基于时间的 hash(用户 [ID](https://github.com/V2Ray/v2ray-core/blob/master/spec/id.md)),见下文 @@ -43,32 +43,9 @@ 其中数据部分使用 AES-128-CFB 加密,IV 为 md5(请求数据 IV),Key 为 md5(请求数据 Key) -## UDP -UDP 数据包为对称设计,即请求和响应的格式一样 - -* 16 字节:基于时间的 hash(用户 [ID](https://github.com/V2Ray/v2ray-core/blob/master/spec/id.md)),见下文 -* 4 字节:余下所有内容的 FNV1a hash -* 1 字节:版本号,目前为 0x1 -* 1 字节:保留,暂为 0x00 -* 2 字节:目标端口 -* 1 字节:目标类型 - * 0x01:IPv4 - * 0x02:域名 - * 0x03:IPv6 -* 目标地址: - * 4 字节:IPv4 - * 1 字节长度 + 域名 - * 16 字节:IPv6 -* N 字节:请求数据 - -其中除了 hash 之外的部分经过 AES-128-CFB 加密: -* Key:md5(用户 ID + '22f01806-5ef0-4e88-95ab-b57f1c7a4a40') -* IV:md5(X + X + X + X),X = []byte(UserHash 生成的时间) (8 字节, Big Endian) - - ## 基于时间的用户 ID Hash * H = MD5 * K = 用户 ID (16 字节) * M = UTC 时间,精确到秒,取值为当前时间的前后 30 秒随机值(8 字节, Big Endian) -* Hash = HMAC(H, K, M) \ No newline at end of file +* Hash = HMAC(H, K, M)