diff --git a/common/collect/timed_queue.go b/common/collect/timed_queue.go index 291d8ad5..c8e16b9c 100644 --- a/common/collect/timed_queue.go +++ b/common/collect/timed_queue.go @@ -39,6 +39,7 @@ func (queue *timedQueueImpl) Pop() interface{} { return v } +// TimedQueue is a priority queue that entries with oldest timestamp get removed first. type TimedQueue struct { queue timedQueueImpl access sync.RWMutex diff --git a/proxy/vmess/inbound.go b/proxy/vmess/inbound/inbound.go similarity index 99% rename from proxy/vmess/inbound.go rename to proxy/vmess/inbound/inbound.go index eabafcf0..8a29ded1 100644 --- a/proxy/vmess/inbound.go +++ b/proxy/vmess/inbound/inbound.go @@ -1,4 +1,4 @@ -package vmess +package inbound import ( "crypto/md5" diff --git a/proxy/vmess/outbound/command.go b/proxy/vmess/outbound/command.go new file mode 100644 index 00000000..8f141571 --- /dev/null +++ b/proxy/vmess/outbound/command.go @@ -0,0 +1,5 @@ +package outbound + +func handleCommand(command byte, data []byte) error { + return nil +} diff --git a/proxy/vmess/outbound.go b/proxy/vmess/outbound/outbound.go similarity index 92% rename from proxy/vmess/outbound.go rename to proxy/vmess/outbound/outbound.go index b7277deb..01b82e03 100644 --- a/proxy/vmess/outbound.go +++ b/proxy/vmess/outbound/outbound.go @@ -1,4 +1,4 @@ -package vmess +package outbound import ( "crypto/md5" @@ -178,7 +178,21 @@ func handleResponse(conn net.Conn, request *protocol.VMessRequest, output chan<- } log.Info("VMessOut received %d bytes from %s", buffer.Len()-4, conn.RemoteAddr().String()) - buffer.SliceFrom(4) + responseBegin := 4 + if buffer.Value[2] != 0 { + dataLen := int(buffer.Value[3]) + if buffer.Len() < dataLen+4 { // Rare case + diffBuffer := make([]byte, dataLen+4-buffer.Len()) + v2net.ReadAllBytes(decryptResponseReader, diffBuffer) + buffer.Append(diffBuffer) + } + command := buffer.Value[2] + data := buffer.Value[4 : 4+dataLen] + go handleCommand(command, data) + responseBegin = 4 + dataLen + } + + buffer.SliceFrom(responseBegin) output <- buffer if !isUDP { diff --git a/proxy/vmess/vmess.go b/proxy/vmess/vmess.go new file mode 100644 index 00000000..51036d5c --- /dev/null +++ b/proxy/vmess/vmess.go @@ -0,0 +1,12 @@ +// 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 + +// The actual implementation is in the following packages respectively. +import ( + _ "github.com/v2ray/v2ray-core/proxy/vmess/inbound" + _ "github.com/v2ray/v2ray-core/proxy/vmess/outbound" +) diff --git a/shell/point/inbound_detour.go b/shell/point/inbound_detour.go index 4d405728..27b752f7 100644 --- a/shell/point/inbound_detour.go +++ b/shell/point/inbound_detour.go @@ -47,13 +47,17 @@ func (this *InboundDetourHandler) Initialize() error { // Starts the inbound connection handler. func (this *InboundDetourHandler) Start() error { for _, ich := range this.ich { - return retry.Timed(100 /* times */, 100 /* ms */).On(func() error { + err := retry.Timed(100 /* times */, 100 /* ms */).On(func() error { err := ich.handler.Listen(ich.port) if err != nil { + log.Error("Failed to start inbound detour on port %d: %v", ich.port, err) return err } return nil }) + if err != nil { + return err + } } return nil } diff --git a/testing/scenarios/data/test_1_client.json b/testing/scenarios/data/test_1_client.json new file mode 100644 index 00000000..6c26e904 --- /dev/null +++ b/testing/scenarios/data/test_1_client.json @@ -0,0 +1,97 @@ +{ + "port": 50000, + "inbound": { + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + }, + "outbound": { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50001, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + "outboundDetour": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50005, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50006, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50007, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50008, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50009, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + } + ] +} diff --git a/testing/scenarios/data/test_1_server.json b/testing/scenarios/data/test_1_server.json new file mode 100644 index 00000000..36841edc --- /dev/null +++ b/testing/scenarios/data/test_1_server.json @@ -0,0 +1,32 @@ +{ + "port": 50001, + "inbound": { + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f", + "level": 1 + } + ] + } + }, + "outbound": { + "protocol": "freedom", + "settings": {} + }, + "inboundDetour": [ + { + "protocol": "vmess", + "port": "50005-50009", + "settings": { + "clients": [ + { + "id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f", + "level": 1 + } + ] + } + } + ] +} diff --git a/testing/scenarios/data/test_2_client.json b/testing/scenarios/data/test_2_client.json new file mode 100644 index 00000000..84e81a2e --- /dev/null +++ b/testing/scenarios/data/test_2_client.json @@ -0,0 +1,37 @@ +{ + "port": 50010, + "inbound": { + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": false, + "ip": "127.0.0.1" + } + }, + "outbound": { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50017, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + "inboundDetour": [ + { + "protocol": "dokodemo-door", + "port": "50011-50015", + "settings": { + "address": "127.0.0.1", + "port": 50016, + "network": "tcp", + "timeout": 0 + } + } + ] +} diff --git a/testing/scenarios/data/test_2_server.json b/testing/scenarios/data/test_2_server.json new file mode 100644 index 00000000..3efc9fb9 --- /dev/null +++ b/testing/scenarios/data/test_2_server.json @@ -0,0 +1,18 @@ +{ + "port": 50017, + "inbound": { + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f", + "level": 1 + } + ] + } + }, + "outbound": { + "protocol": "freedom", + "settings": {} + } +} diff --git a/testing/scenarios/data/test_3_client.json b/testing/scenarios/data/test_3_client.json new file mode 100644 index 00000000..60f3d8f2 --- /dev/null +++ b/testing/scenarios/data/test_3_client.json @@ -0,0 +1,57 @@ +{ + "port": 50020, + "inbound": { + "protocol": "dokodemo-door", + "settings": { + "address": "127.0.0.1", + "port": 50024, + "network": "tcp", + "timeout": 0 + } + }, + "outbound": { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 50021, + "users": [ + {"id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f"} + ] + } + ] + } + }, + "inboundDetour": [ + { + "protocol": "dokodemo-door", + "port": 50022, + "settings": { + "address": "127.0.0.1", + "port": 50025, + "network": "tcp", + "timeout": 0 + } + } + ], + "outboundDetour": [ + { + "protocol": "blackhole", + "tag": "blocked", + "settings": {} + } + ], + "routing": { + "strategy": "rules", + "settings": { + "rules": [ + { + "type": "field", + "port": "50025-50029", + "outboundTag": "blocked" + } + ] + } + } +} diff --git a/testing/scenarios/data/test_3_server.json b/testing/scenarios/data/test_3_server.json new file mode 100644 index 00000000..d3f21ea6 --- /dev/null +++ b/testing/scenarios/data/test_3_server.json @@ -0,0 +1,18 @@ +{ + "port": 50021, + "inbound": { + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "d17a1af7-efa5-42ca-b7e9-6a35282d737f", + "level": 1 + } + ] + } + }, + "outbound": { + "protocol": "freedom", + "settings": {} + } +} diff --git a/testing/scenarios/dokodemo_test.go b/testing/scenarios/dokodemo_test.go new file mode 100644 index 00000000..26eaf938 --- /dev/null +++ b/testing/scenarios/dokodemo_test.go @@ -0,0 +1,52 @@ +package scenarios + +import ( + "net" + "testing" + + v2net "github.com/v2ray/v2ray-core/common/net" + v2testing "github.com/v2ray/v2ray-core/testing" + "github.com/v2ray/v2ray-core/testing/assert" + "github.com/v2ray/v2ray-core/testing/servers/tcp" +) + +func TestDokodemoTCP(t *testing.T) { + v2testing.Current(t) + + tcpServer := &tcp.Server{ + Port: v2net.Port(50016), + MsgProcessor: func(data []byte) []byte { + buffer := make([]byte, 0, 2048) + buffer = append(buffer, []byte("Processed: ")...) + buffer = append(buffer, data...) + return buffer + }, + } + _, err := tcpServer.Start() + assert.Error(err).IsNil() + + assert.Error(InitializeServerSetOnce("test_2")).IsNil() + + dokodemoPortStart := v2net.Port(50011) + dokodemoPortEnd := v2net.Port(50015) + + for port := dokodemoPortStart; port <= dokodemoPortEnd; port++ { + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(port), + }) + + payload := "dokodemo request." + nBytes, err := conn.Write([]byte(payload)) + assert.Error(err).IsNil() + assert.Int(nBytes).Equals(len(payload)) + + conn.CloseWrite() + + response := make([]byte, 1024) + nBytes, err = conn.Read(response) + assert.Error(err).IsNil() + assert.StringLiteral("Processed: " + payload).Equals(string(response[:nBytes])) + conn.Close() + } +} diff --git a/testing/scenarios/router_test.go b/testing/scenarios/router_test.go new file mode 100644 index 00000000..831fb2e8 --- /dev/null +++ b/testing/scenarios/router_test.go @@ -0,0 +1,77 @@ +package scenarios + +import ( + "net" + "testing" + + v2net "github.com/v2ray/v2ray-core/common/net" + v2testing "github.com/v2ray/v2ray-core/testing" + "github.com/v2ray/v2ray-core/testing/assert" + "github.com/v2ray/v2ray-core/testing/servers/tcp" +) + +func TestRouter(t *testing.T) { + v2testing.Current(t) + + tcpServer := &tcp.Server{ + Port: v2net.Port(50024), + MsgProcessor: func(data []byte) []byte { + buffer := make([]byte, 0, 2048) + buffer = append(buffer, []byte("Processed: ")...) + buffer = append(buffer, data...) + return buffer + }, + } + _, err := tcpServer.Start() + assert.Error(err).IsNil() + + tcpServer2Accessed := false + tcpServer2 := &tcp.Server{ + Port: v2net.Port(50025), + MsgProcessor: func(data []byte) []byte { + tcpServer2Accessed = true + return data + }, + } + _, err = tcpServer2.Start() + assert.Error(err).IsNil() + + assert.Error(InitializeServerSetOnce("test_3")).IsNil() + + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(50020), + }) + + payload := "direct dokodemo request." + nBytes, err := conn.Write([]byte(payload)) + assert.Error(err).IsNil() + assert.Int(nBytes).Equals(len(payload)) + + conn.CloseWrite() + + response := make([]byte, 1024) + nBytes, err = conn.Read(response) + assert.Error(err).IsNil() + assert.StringLiteral("Processed: " + payload).Equals(string(response[:nBytes])) + conn.Close() + + conn, err = net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(50022), + }) + + payload = "blocked dokodemo request." + nBytes, err = conn.Write([]byte(payload)) + assert.Error(err).IsNil() + assert.Int(nBytes).Equals(len(payload)) + + conn.CloseWrite() + + response = make([]byte, 1024) + nBytes, err = conn.Read(response) + assert.Error(err).IsNotNil() + assert.Int(nBytes).Equals(0) + assert.Bool(tcpServer2Accessed).IsFalse() + conn.Close() +} diff --git a/testing/scenarios/server_env.go b/testing/scenarios/server_env.go new file mode 100644 index 00000000..2d8489f4 --- /dev/null +++ b/testing/scenarios/server_env.go @@ -0,0 +1,71 @@ +package scenarios + +import ( + "os" + "path/filepath" + + _ "github.com/v2ray/v2ray-core/app/router/config/json" + _ "github.com/v2ray/v2ray-core/app/router/rules" + _ "github.com/v2ray/v2ray-core/app/router/rules/config/json" + "github.com/v2ray/v2ray-core/common/log" + "github.com/v2ray/v2ray-core/shell/point" + jsonconf "github.com/v2ray/v2ray-core/shell/point/config/json" + + // The following are neccesary as they register handlers in their init functions. + _ "github.com/v2ray/v2ray-core/proxy/blackhole" + _ "github.com/v2ray/v2ray-core/proxy/blackhole/config/json" + _ "github.com/v2ray/v2ray-core/proxy/dokodemo" + _ "github.com/v2ray/v2ray-core/proxy/dokodemo/config/json" + _ "github.com/v2ray/v2ray-core/proxy/freedom" + _ "github.com/v2ray/v2ray-core/proxy/freedom/config/json" + _ "github.com/v2ray/v2ray-core/proxy/socks" + _ "github.com/v2ray/v2ray-core/proxy/socks/config/json" + _ "github.com/v2ray/v2ray-core/proxy/vmess" + _ "github.com/v2ray/v2ray-core/proxy/vmess/config/json" +) + +var ( + serverup = make(map[string]bool) +) + +func TestFile(filename string) string { + return filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "v2ray", "v2ray-core", "testing", "scenarios", "data", filename) +} + +func InitializeServerSetOnce(testcase string) error { + if up, found := serverup[testcase]; found && up { + return nil + } + err := InitializeServer(TestFile(testcase + "_server.json")) + if err != nil { + return err + } + err = InitializeServer(TestFile(testcase + "_client.json")) + if err != nil { + return err + } + serverup[testcase] = true + return nil +} + +func InitializeServer(configFile string) error { + config, err := jsonconf.LoadConfig(configFile) + if err != nil { + log.Error("Failed to read config file (%s): %v", configFile, err) + return err + } + + vPoint, err := point.NewPoint(config) + if err != nil { + log.Error("Failed to create Point server: %v", err) + return err + } + + err = vPoint.Start() + if err != nil { + log.Error("Error starting Point server: %v", err) + return err + } + + return nil +} diff --git a/testing/scenarios/socks5_helper.go b/testing/scenarios/socks5_helper.go index 54c4cf85..0faceb70 100644 --- a/testing/scenarios/socks5_helper.go +++ b/testing/scenarios/socks5_helper.go @@ -1,21 +1,7 @@ package scenarios import ( - "net" - - routerconfig "github.com/v2ray/v2ray-core/app/router/config/testing" - _ "github.com/v2ray/v2ray-core/app/router/rules" - rulesconfig "github.com/v2ray/v2ray-core/app/router/rules/config/testing" v2net "github.com/v2ray/v2ray-core/common/net" - v2nettesting "github.com/v2ray/v2ray-core/common/net/testing" - _ "github.com/v2ray/v2ray-core/proxy/freedom" - _ "github.com/v2ray/v2ray-core/proxy/socks" - socksjson "github.com/v2ray/v2ray-core/proxy/socks/config/json" - _ "github.com/v2ray/v2ray-core/proxy/vmess" - "github.com/v2ray/v2ray-core/proxy/vmess/config" - vmessjson "github.com/v2ray/v2ray-core/proxy/vmess/config/json" - "github.com/v2ray/v2ray-core/shell/point" - "github.com/v2ray/v2ray-core/shell/point/config/testing/mocks" ) const ( @@ -60,113 +46,3 @@ func socks5UDPRequest(address v2net.Address, payload []byte) []byte { request = append(request, payload...) return request } - -func setUpV2Ray(routing func(v2net.Destination) bool) (v2net.Port, v2net.Port, error) { - id1, err := config.NewID("ad937d9d-6e23-4a5a-ba23-bce5092a7c51") - if err != nil { - return 0, 0, err - } - id2, err := config.NewID("93ccfc71-b136-4015-ac85-e037bd1ead9e") - if err != nil { - return 0, 0, err - } - users := []*vmessjson.ConfigUser{ - &vmessjson.ConfigUser{Id: id1}, - &vmessjson.ConfigUser{Id: id2}, - } - - portB := v2nettesting.PickPort() - configB := mocks.Config{ - PortValue: portB, - InboundConfigValue: &mocks.ConnectionConfig{ - ProtocolValue: "vmess", - SettingsValue: &vmessjson.Inbound{ - AllowedClients: users, - }, - }, - OutboundConfigValue: &mocks.ConnectionConfig{ - ProtocolValue: "freedom", - SettingsValue: nil, - }, - } - pointB, err := point.NewPoint(&configB) - if err != nil { - return 0, 0, err - } - err = pointB.Start() - if err != nil { - return 0, 0, err - } - - portA := v2nettesting.PickPort() - portA2 := v2nettesting.PickPort() - configA := mocks.Config{ - PortValue: portA, - InboundConfigValue: &mocks.ConnectionConfig{ - ProtocolValue: "socks", - SettingsValue: &socksjson.SocksConfig{ - AuthMethod: "noauth", - UDP: true, - HostIP: socksjson.IPAddress(net.IPv4(127, 0, 0, 1)), - }, - }, - OutboundConfigValue: &mocks.ConnectionConfig{ - ProtocolValue: "vmess", - SettingsValue: &vmessjson.Outbound{ - []*vmessjson.ConfigTarget{ - &vmessjson.ConfigTarget{ - Address: v2net.IPAddress([]byte{127, 0, 0, 1}, portB), - Users: users, - }, - }, - }, - }, - InboundDetoursValue: []*mocks.InboundDetourConfig{ - &mocks.InboundDetourConfig{ - PortRangeValue: &mocks.PortRange{ - FromValue: portA2, - ToValue: portA2, - }, - ConnectionConfig: &mocks.ConnectionConfig{ - ProtocolValue: "socks", - SettingsValue: &socksjson.SocksConfig{ - AuthMethod: "noauth", - UDP: false, - HostIP: socksjson.IPAddress(net.IPv4(127, 0, 0, 1)), - }, - }, - }, - }, - OutboundDetoursValue: []*mocks.OutboundDetourConfig{ - &mocks.OutboundDetourConfig{ - TagValue: "direct", - ConnectionConfig: &mocks.ConnectionConfig{ - ProtocolValue: "freedom", - SettingsValue: nil, - }, - }, - }, - RouterConfigValue: &routerconfig.RouterConfig{ - StrategyValue: "rules", - SettingsValue: &rulesconfig.RouterRuleConfig{ - RuleList: []*rulesconfig.TestRule{ - &rulesconfig.TestRule{ - TagValue: "direct", - Function: routing, - }, - }, - }, - }, - } - - pointA, err := point.NewPoint(&configA) - if err != nil { - return 0, 0, err - } - err = pointA.Start() - if err != nil { - return 0, 0, err - } - - return portA, portA2, nil -} diff --git a/testing/scenarios/socks_end_test.go b/testing/scenarios/socks_end_test.go index 3553d5b3..c8b58e02 100644 --- a/testing/scenarios/socks_end_test.go +++ b/testing/scenarios/socks_end_test.go @@ -13,9 +13,7 @@ import ( ) var ( - EmptyRouting = func(v2net.Destination) bool { - return false - } + serverUp = false ) func TestTCPConnection(t *testing.T) { @@ -34,13 +32,14 @@ func TestTCPConnection(t *testing.T) { _, err := tcpServer.Start() assert.Error(err).IsNil() - v2rayPort, _, err := setUpV2Ray(EmptyRouting) - assert.Error(err).IsNil() + assert.Error(InitializeServerSetOnce("test_1")).IsNil() + + socksPort := v2net.Port(50000) for i := 0; i < 100; i++ { conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ IP: []byte{127, 0, 0, 1}, - Port: int(v2rayPort), + Port: int(socksPort), }) authRequest := socks5AuthMethodRequest(byte(0)) @@ -100,12 +99,13 @@ func TestTCPBind(t *testing.T) { _, err := tcpServer.Start() assert.Error(err).IsNil() - v2rayPort, _, err := setUpV2Ray(EmptyRouting) - assert.Error(err).IsNil() + assert.Error(InitializeServerSetOnce("test_1")).IsNil() + + socksPort := v2net.Port(50000) conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ IP: []byte{127, 0, 0, 1}, - Port: int(v2rayPort), + Port: int(socksPort), }) authRequest := socks5AuthMethodRequest(byte(0)) @@ -147,12 +147,13 @@ func TestUDPAssociate(t *testing.T) { _, err := udpServer.Start() assert.Error(err).IsNil() - v2rayPort, _, err := setUpV2Ray(EmptyRouting) - assert.Error(err).IsNil() + assert.Error(InitializeServerSetOnce("test_1")).IsNil() + + socksPort := v2net.Port(50000) conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ IP: []byte{127, 0, 0, 1}, - Port: int(v2rayPort), + Port: int(socksPort), }) authRequest := socks5AuthMethodRequest(byte(0)) @@ -173,11 +174,11 @@ func TestUDPAssociate(t *testing.T) { connectResponse := make([]byte, 1024) nBytes, err = conn.Read(connectResponse) assert.Error(err).IsNil() - assert.Bytes(connectResponse[:nBytes]).Equals([]byte{socks5Version, 0, 0, 1, 127, 0, 0, 1, byte(v2rayPort >> 8), byte(v2rayPort)}) + assert.Bytes(connectResponse[:nBytes]).Equals([]byte{socks5Version, 0, 0, 1, 127, 0, 0, 1, byte(socksPort >> 8), byte(socksPort)}) udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: []byte{127, 0, 0, 1}, - Port: int(v2rayPort), + Port: int(socksPort), }) assert.Error(err).IsNil()