diff --git a/common/badjson/json.go b/common/badjson/json.go index a6d6a776..1c299227 100644 --- a/common/badjson/json.go +++ b/common/badjson/json.go @@ -17,12 +17,16 @@ func decodeJSON(decoder *json.Decoder) (any, error) { case '{': var object JSONObject err = object.decodeJSON(decoder) + if err != nil { + return nil, err + } + rawToken, err = decoder.Token() if err != nil { return nil, err } else if rawToken != json.Delim('}') { return nil, E.New("excepted object end, but got ", rawToken) } - return object, nil + return &object, nil case '[': var array JSONArray[any] err = array.decodeJSON(decoder) @@ -35,7 +39,7 @@ func decodeJSON(decoder *json.Decoder) (any, error) { } else if rawToken != json.Delim(']') { return nil, E.New("excepted array end, but got ", rawToken) } - return &array, nil + return array, nil default: return nil, E.New("excepted object or array end: ", token) } diff --git a/test/box_test.go b/test/box_test.go index e6106028..ec5087f7 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -54,11 +54,25 @@ func testSuit(t *testing.T, clientPort uint16, testPort uint16) { } t.Run("tcp", func(t *testing.T) { t.Parallel() - require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) + var err error + for retry := 0; retry < 3; retry++ { + err = testLargeDataWithConn(t, testPort, dialTCP) + if err == nil { + break + } + } + require.NoError(t, err) }) t.Run("udp", func(t *testing.T) { t.Parallel() - require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) + var err error + for retry := 0; retry < 3; retry++ { + err = testLargeDataWithPacketConn(t, testPort, dialUDP) + if err == nil { + break + } + } + require.NoError(t, err) }) // require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) // require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) diff --git a/test/clash_test.go b/test/clash_test.go index 833dab73..4055f063 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/control" F "github.com/sagernet/sing/common/format" "github.com/docker/docker/api/types" @@ -27,11 +28,13 @@ import ( const ( ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest" ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest" + ImageV2RayCore = "v2fly/v2fly-core:latest" ) var allImages = []string{ ImageShadowsocksRustServer, ImageShadowsocksRustClient, + ImageV2RayCore, } var ( @@ -200,7 +203,9 @@ func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error) func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { l, err := listenPacket("udp", ":"+F.ToString(port)) - require.NoError(t, err) + if err != nil { + return err + } defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} @@ -345,7 +350,9 @@ func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { l, err := listenPacket("udp", ":"+F.ToString(port)) - require.NoError(t, err) + if err != nil { + return err + } defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} @@ -467,8 +474,8 @@ func testPacketConnTimeout(t *testing.T, pcc func() (net.PacketConn, error)) err } func listen(network, address string) (net.Listener, error) { - lc := net.ListenConfig{} - + var lc net.ListenConfig + lc.Control = control.ReuseAddr() var lastErr error for i := 0; i < 5; i++ { l, err := lc.Listen(context.Background(), network, address) @@ -483,9 +490,11 @@ func listen(network, address string) (net.Listener, error) { } func listenPacket(network, address string) (net.PacketConn, error) { + var lc net.ListenConfig + lc.Control = control.ReuseAddr() var lastErr error for i := 0; i < 5; i++ { - l, err := net.ListenPacket(network, address) + l, err := lc.ListenPacket(context.Background(), network, address) if err == nil { return l, nil } diff --git a/test/config/vmess-client.json b/test/config/vmess-client.json new file mode 100644 index 00000000..b2787e2c --- /dev/null +++ b/test/config/vmess-client.json @@ -0,0 +1,37 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": "1080", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "ip": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "127.0.0.1", + "port": 1234, + "users": [ + { + "id": "", + "security": "", + "experiments": "" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-server.json b/test/config/vmess-server.json new file mode 100644 index 00000000..3d971e8a --- /dev/null +++ b/test/config/vmess-server.json @@ -0,0 +1,25 @@ +{ + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 1234, + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "alterId": 0 + } + ] + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/docker_test.go b/test/docker_test.go index 87bfbc15..10d41eb2 100644 --- a/test/docker_test.go +++ b/test/docker_test.go @@ -21,6 +21,7 @@ type DockerOptions struct { Cmd []string Env []string Bind []string + Stdin []byte } func startDockerContainer(t *testing.T, options DockerOptions) { @@ -28,9 +29,19 @@ func startDockerContainer(t *testing.T, options DockerOptions) { require.NoError(t, err) defer dockerClient.Close() + writeStdin := len(options.Stdin) > 0 + var containerOptions container.Config + + if writeStdin { + containerOptions.OpenStdin = true + containerOptions.StdinOnce = true + } + containerOptions.Image = options.Image - containerOptions.Entrypoint = []string{options.EntryPoint} + if options.EntryPoint != "" { + containerOptions.Entrypoint = []string{options.EntryPoint} + } containerOptions.Cmd = options.Cmd containerOptions.Env = options.Env containerOptions.ExposedPorts = make(nat.PortSet) @@ -57,13 +68,29 @@ func startDockerContainer(t *testing.T, options DockerOptions) { t.Cleanup(func() { cleanContainer(dockerContainer.ID) }) + require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, types.ContainerStartOptions{})) + + if writeStdin { + stdinAttach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, types.ContainerAttachOptions{ + Stdin: writeStdin, + Stream: true, + }) + require.NoError(t, err) + _, err = stdinAttach.Conn.Write(options.Stdin) + require.NoError(t, err) + stdinAttach.Close() + } + /*attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, types.ContainerAttachOptions{ - Logs: true, Stream: true, Stdout: true, Stderr: true, + Stdout: true, + Stderr: true, + Logs: true, + Stream: true, }) require.NoError(t, err) go func() { - attach.Reader.WriteTo(os.Stderr) + stdcopy.StdCopy(os.Stderr, os.Stderr, attach.Reader) }()*/ time.Sleep(time.Second) } diff --git a/test/go.mod b/test/go.mod index cdeff46b..4256bf3d 100644 --- a/test/go.mod +++ b/test/go.mod @@ -11,6 +11,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid v4.2.0+incompatible github.com/sagernet/sing v0.0.0-20220718035659-3d74b823ed56 + github.com/spyzhov/ajson v0.7.1 github.com/stretchr/testify v1.8.0 golang.org/x/net v0.0.0-20220708220712-1185a9018129 ) diff --git a/test/go.sum b/test/go.sum index 484c5afb..3eba11d8 100644 --- a/test/go.sum +++ b/test/go.sum @@ -68,6 +68,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spyzhov/ajson v0.7.1 h1:1MDIlPc6x0zjNtpa7tDzRAyFAvRX+X8ZsvtYz5lZg6A= +github.com/spyzhov/ajson v0.7.1/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/test/vmess_test.go b/test/vmess_test.go index 12cba263..17ce3edf 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -2,64 +2,232 @@ package main import ( "net/netip" + "os" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/gofrs/uuid" + "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" ) -func TestVMessSelf(t *testing.T) { +func TestVMess(t *testing.T) { t.Parallel() for _, security := range []string{ "zero", } { t.Run(security, func(t *testing.T) { - testVMessSelf0(t, security) + testVMess0(t, security) }) } for _, security := range []string{ "aes-128-gcm", "chacha20-poly1305", "aes-128-cfb", } { t.Run(security, func(t *testing.T) { - testVMessSelf1(t, security) + testVMess1(t, security) }) } } -func testVMessSelf0(t *testing.T, security string) { +func testVMess0(t *testing.T, security string) { t.Parallel() user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) - t.Run("default", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), false, false) + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, user, false, false) }) - t.Run("padding", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), true, false) + t.Run("self-padding", func(t *testing.T) { + testVMessSelf(t, security, user, true, false) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, false, 0) + }) + t.Run("outbound-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, false, 0) + }) + t.Run("outbound-legacy", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, false, 1) + }) + t.Run("outbound-legacy-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, false, 1) }) } -func testVMessSelf1(t *testing.T, security string) { +func testVMess1(t *testing.T, security string) { t.Parallel() user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) - t.Run("default", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), false, false) + t.Run("self", func(t *testing.T) { + testVMessSelf(t, security, user, false, false) }) - t.Run("padding", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), true, false) + t.Run("self-padding", func(t *testing.T) { + testVMessSelf(t, security, user, true, false) }) - t.Run("authid", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), false, true) + t.Run("self-authid", func(t *testing.T) { + testVMessSelf(t, security, user, false, true) }) - t.Run("padding-authid", func(t *testing.T) { - testVMessSelf2(t, security, user.String(), true, true) + t.Run("self-padding-authid", func(t *testing.T) { + testVMessSelf(t, security, user, true, true) + }) + t.Run("inbound", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, user, false) + }) + t.Run("inbound-authid", func(t *testing.T) { + testVMessInboundWithV2Ray(t, security, user, true) + }) + t.Run("outbound", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, false, 0) + }) + t.Run("outbound-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, false, 0) + }) + t.Run("outbound-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, true, 0) + }) + t.Run("outbound-padding-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, true, 0) + }) + t.Run("outbound-legacy", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, false, 1) + }) + t.Run("outbound-legacy-padding", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, false, 1) + }) + t.Run("outbound-legacy-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, false, true, 1) + }) + t.Run("outbound-legacy-padding-authid", func(t *testing.T) { + testVMessOutboundWithV2Ray(t, security, user, true, true, 1) }) } -func testVMessSelf2(t *testing.T, security string, uuid string, globalPadding bool, authenticatedLength bool) { +func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, authenticatedLength bool) { + t.Parallel() + + content, err := os.ReadFile("config/vmess-client.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + serverPort := mkPort(t) + clientPort := mkPort(t) + testPort := mkPort(t) + + config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) + outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) + outbound.MustKey("port").SetNumeric(float64(serverPort)) + user := outbound.MustKey("users").MustIndex(0) + user.MustKey("id").SetString(uuid.String()) + user.MustKey("security").SetString(security) + var experiments string + if authenticatedLength { + experiments += "AuthenticatedLength" + } + user.MustKey("experiments").SetString(experiments) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + }) + + startInstance(t, option.Options{ + Log: &option.LogOption{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeVMess, + VMessOptions: option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.VMessUser{ + { + Name: "sekai", + UUID: uuid.String(), + }, + }, + }, + }, + }, + }) + + testSuit(t, clientPort, testPort) +} + +func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, globalPadding bool, authenticatedLength bool, alterId int) { + t.Parallel() + + content, err := os.ReadFile("config/vmess-server.json") + require.NoError(t, err) + config, err := ajson.Unmarshal(content) + require.NoError(t, err) + + serverPort := mkPort(t) + clientPort := mkPort(t) + testPort := mkPort(t) + + inbound := config.MustKey("inbounds").MustIndex(0) + inbound.MustKey("port").SetNumeric(float64(serverPort)) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(uuid.String()) + inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("alterId").SetNumeric(float64(alterId)) + + content, err = ajson.Marshal(config) + require.NoError(t, err) + + startDockerContainer(t, DockerOptions{ + Image: ImageV2RayCore, + Ports: []uint16{serverPort, testPort}, + EntryPoint: "v2ray", + Stdin: content, + Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, + }) + + startInstance(t, option.Options{ + Log: &option.LogOption{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeVMess, + VMessOptions: option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Security: security, + UUID: uuid.String(), + GlobalPadding: globalPadding, + AuthenticatedLength: authenticatedLength, + AlterId: alterId, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, globalPadding bool, authenticatedLength bool) { t.Parallel() serverPort := mkPort(t) clientPort := mkPort(t) @@ -89,7 +257,7 @@ func testVMessSelf2(t *testing.T, security string, uuid string, globalPadding bo Users: []option.VMessUser{ { Name: "sekai", - UUID: uuid, + UUID: uuid.String(), }, }, }, @@ -108,7 +276,7 @@ func testVMessSelf2(t *testing.T, security string, uuid string, globalPadding bo ServerPort: serverPort, }, Security: security, - UUID: uuid, + UUID: uuid.String(), GlobalPadding: globalPadding, AuthenticatedLength: authenticatedLength, },