Ver Fonte

Add shadowsocks tests

世界 há 3 anos atrás
pai
commit
7f8c9ffa30
10 ficheiros alterados com 1063 adições e 2 exclusões
  1. 1 1
      inbound/default.go
  2. 1 1
      option/inbound.go
  3. 54 0
      test/box_test.go
  4. 71 0
      test/clash_darwin_test.go
  5. 12 0
      test/clash_other_test.go
  6. 499 0
      test/clash_test.go
  7. 76 0
      test/docker_test.go
  8. 42 0
      test/go.mod
  9. 118 0
      test/go.sum
  10. 189 0
      test/shadowsocks_test.go

+ 1 - 1
inbound/default.go

@@ -54,7 +54,7 @@ func (a *myInboundAdapter) Tag() string {
 }
 
 func (a *myInboundAdapter) Start() error {
-	bindAddr := M.SocksaddrFromAddrPort(netip.Addr(a.listenOptions.Listen), a.listenOptions.Port)
+	bindAddr := M.SocksaddrFromAddrPort(netip.Addr(a.listenOptions.Listen), a.listenOptions.ListenPort)
 	if common.Contains(a.network, C.NetworkTCP) {
 		var tcpListener *net.TCPListener
 		var err error

+ 1 - 1
option/inbound.go

@@ -79,7 +79,7 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 
 type ListenOptions struct {
 	Listen                   ListenAddress  `json:"listen"`
-	Port                     uint16         `json:"listen_port"`
+	ListenPort               uint16         `json:"listen_port"`
 	TCPFastOpen              bool           `json:"tcp_fast_open,omitempty"`
 	UDPTimeout               int64          `json:"udp_timeout,omitempty"`
 	SniffEnabled             bool           `json:"sniff,omitempty"`

+ 54 - 0
test/box_test.go

@@ -0,0 +1,54 @@
+package main
+
+import (
+	"context"
+	"net"
+	"testing"
+
+	"github.com/sagernet/sing-box"
+	"github.com/sagernet/sing-box/option"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/protocol/socks"
+
+	"github.com/stretchr/testify/require"
+)
+
+func mkPort(t *testing.T) uint16 {
+	for {
+		tcpListener, err := net.ListenTCP("tcp", nil)
+		require.NoError(t, err)
+		listenPort := M.SocksaddrFromNet(tcpListener.Addr()).Port
+		tcpListener.Close()
+		udpListener, err := net.ListenUDP("udp", &net.UDPAddr{Port: int(listenPort)})
+		if err != nil {
+			continue
+		}
+		udpListener.Close()
+		return listenPort
+	}
+}
+
+func startInstance(t *testing.T, options option.Options) {
+	instance, err := box.New(context.Background(), options)
+	require.NoError(t, err)
+	require.NoError(t, instance.Start())
+	t.Cleanup(func() {
+		instance.Close()
+	})
+}
+
+func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
+	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
+	dialTCP := func() (net.Conn, error) {
+		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	}
+	dialUDP := func() (net.PacketConn, error) {
+		return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	}
+	require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
+	require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
+	require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
+	require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
+	require.NoError(t, testPacketConnTimeout(t, dialUDP))
+}

+ 71 - 0
test/clash_darwin_test.go

@@ -0,0 +1,71 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"net/netip"
+	"syscall"
+
+	"golang.org/x/net/route"
+)
+
+func defaultRouteIP() (netip.Addr, error) {
+	idx, err := defaultRouteInterfaceIndex()
+	if err != nil {
+		return netip.Addr{}, err
+	}
+	iface, err := net.InterfaceByIndex(idx)
+	if err != nil {
+		return netip.Addr{}, err
+	}
+	addrs, err := iface.Addrs()
+	if err != nil {
+		return netip.Addr{}, err
+	}
+	for _, addr := range addrs {
+		ip := addr.(*net.IPNet).IP
+		if ip.To4() != nil {
+			return netip.AddrFrom4(*(*[4]byte)(ip)), nil
+		}
+	}
+
+	return netip.Addr{}, errors.New("no ipv4 addr")
+}
+
+func defaultRouteInterfaceIndex() (int, error) {
+	rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
+	if err != nil {
+		return 0, fmt.Errorf("route.FetchRIB: %w", err)
+	}
+	msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
+	if err != nil {
+		return 0, fmt.Errorf("route.ParseRIB: %w", err)
+	}
+	for _, message := range msgs {
+		routeMessage := message.(*route.RouteMessage)
+		if routeMessage.Flags&(syscall.RTF_UP|syscall.RTF_GATEWAY|syscall.RTF_STATIC) == 0 {
+			continue
+		}
+
+		addresses := routeMessage.Addrs
+
+		destination, ok := addresses[0].(*route.Inet4Addr)
+		if !ok {
+			continue
+		}
+
+		if destination.IP != [4]byte{0, 0, 0, 0} {
+			continue
+		}
+
+		switch addresses[1].(type) {
+		case *route.Inet4Addr:
+			return routeMessage.Index, nil
+		default:
+			continue
+		}
+	}
+
+	return 0, fmt.Errorf("ambiguous gateway interfaces found")
+}

+ 12 - 0
test/clash_other_test.go

@@ -0,0 +1,12 @@
+//go:build !darwin
+
+package main
+
+import (
+	"errors"
+	"net/netip"
+)
+
+func defaultRouteIP() (netip.Addr, error) {
+	return netip.Addr{}, errors.New("not supported")
+}

+ 499 - 0
test/clash_test.go

@@ -0,0 +1,499 @@
+package main
+
+import (
+	"context"
+	"crypto/md5"
+	"crypto/rand"
+	"errors"
+	"io"
+	"net"
+	"net/netip"
+	"runtime"
+	"sync"
+	"testing"
+	"time"
+
+	F "github.com/sagernet/sing/common/format"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// kanged from clash
+
+const (
+	ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest"
+	ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest"
+)
+
+var allImages = []string{
+	ImageShadowsocksRustServer,
+	ImageShadowsocksRustClient,
+}
+
+var (
+	localIP  = netip.MustParseAddr("127.0.0.1")
+	isDarwin = runtime.GOOS == "darwin"
+)
+
+func init() {
+	if isDarwin {
+		var err error
+		localIP, err = defaultRouteIP()
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
+	if err != nil {
+		panic(err)
+	}
+	defer dockerClient.Close()
+
+	list, err := dockerClient.ImageList(context.Background(), types.ImageListOptions{All: true})
+	if err != nil {
+		panic(err)
+	}
+
+	imageExist := func(image string) bool {
+		for _, item := range list {
+			for _, tag := range item.RepoTags {
+				if image == tag {
+					return true
+				}
+			}
+		}
+		return false
+	}
+
+	for _, image := range allImages {
+		if imageExist(image) {
+			continue
+		}
+
+		logrus.Info("pulling image: ", image)
+		imageStream, err := dockerClient.ImagePull(context.Background(), image, types.ImagePullOptions{})
+		if err != nil {
+			panic(err)
+		}
+
+		io.Copy(io.Discard, imageStream)
+	}
+}
+
+func newPingPongPair() (chan []byte, chan []byte, func(t *testing.T) error) {
+	pingCh := make(chan []byte)
+	pongCh := make(chan []byte)
+	test := func(t *testing.T) error {
+		defer close(pingCh)
+		defer close(pongCh)
+		pingOpen := false
+		pongOpen := false
+		var recv []byte
+
+		for {
+			if pingOpen && pongOpen {
+				break
+			}
+
+			select {
+			case recv, pingOpen = <-pingCh:
+				assert.True(t, pingOpen)
+				assert.Equal(t, []byte("ping"), recv)
+			case recv, pongOpen = <-pongCh:
+				assert.True(t, pongOpen)
+				assert.Equal(t, []byte("pong"), recv)
+			case <-time.After(10 * time.Second):
+				return errors.New("timeout")
+			}
+		}
+		return nil
+	}
+
+	return pingCh, pongCh, test
+}
+
+func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error) {
+	pingCh := make(chan hashPair)
+	pongCh := make(chan hashPair)
+	test := func(t *testing.T) error {
+		defer close(pingCh)
+		defer close(pongCh)
+		pingOpen := false
+		pongOpen := false
+		var serverPair hashPair
+		var clientPair hashPair
+
+		for {
+			if pingOpen && pongOpen {
+				break
+			}
+
+			select {
+			case serverPair, pingOpen = <-pingCh:
+				assert.True(t, pingOpen)
+			case clientPair, pongOpen = <-pongCh:
+				assert.True(t, pongOpen)
+			case <-time.After(10 * time.Second):
+				return errors.New("timeout")
+			}
+		}
+
+		assert.Equal(t, serverPair.recvHash, clientPair.sendHash)
+		assert.Equal(t, serverPair.sendHash, clientPair.recvHash)
+
+		return nil
+	}
+
+	return pingCh, pongCh, test
+}
+
+func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error {
+	l, err := listen("tcp", ":"+F.ToString(port))
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	c, err := cc()
+	if err != nil {
+		return err
+	}
+
+	pingCh, pongCh, test := newPingPongPair()
+	go func() {
+		c, err := l.Accept()
+		if err != nil {
+			return
+		}
+
+		buf := make([]byte, 4)
+		if _, err := io.ReadFull(c, buf); err != nil {
+			return
+		}
+
+		pingCh <- buf
+		if _, err := c.Write([]byte("pong")); err != nil {
+			return
+		}
+	}()
+
+	go func() {
+		if _, err := c.Write([]byte("ping")); err != nil {
+			return
+		}
+
+		buf := make([]byte, 4)
+		if _, err := io.ReadFull(c, buf); err != nil {
+			return
+		}
+
+		pongCh <- buf
+	}()
+
+	return test(t)
+}
+
+func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error {
+	l, err := listenPacket("udp", ":"+F.ToString(port))
+	require.NoError(t, err)
+	defer l.Close()
+
+	rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)}
+
+	pingCh, pongCh, test := newPingPongPair()
+	go func() {
+		buf := make([]byte, 1024)
+		n, rAddr, err := l.ReadFrom(buf)
+		if err != nil {
+			return
+		}
+
+		pingCh <- buf[:n]
+		if _, err := l.WriteTo([]byte("pong"), rAddr); err != nil {
+			return
+		}
+	}()
+
+	pc, err := pcc()
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		if _, err := pc.WriteTo([]byte("ping"), rAddr); err != nil {
+			return
+		}
+
+		buf := make([]byte, 1024)
+		n, _, err := pc.ReadFrom(buf)
+		if err != nil {
+			return
+		}
+
+		pongCh <- buf[:n]
+	}()
+
+	return test(t)
+}
+
+type hashPair struct {
+	sendHash map[int][]byte
+	recvHash map[int][]byte
+}
+
+func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error {
+	l, err := listen("tcp", ":"+F.ToString(port))
+	require.NoError(t, err)
+	defer l.Close()
+
+	times := 100
+	chunkSize := int64(64 * 1024)
+
+	pingCh, pongCh, test := newLargeDataPair()
+	writeRandData := func(conn net.Conn) (map[int][]byte, error) {
+		buf := make([]byte, chunkSize)
+		hashMap := map[int][]byte{}
+		for i := 0; i < times; i++ {
+			if _, err := rand.Read(buf[1:]); err != nil {
+				return nil, err
+			}
+			buf[0] = byte(i)
+
+			hash := md5.Sum(buf)
+			hashMap[i] = hash[:]
+
+			if _, err := conn.Write(buf); err != nil {
+				return nil, err
+			}
+		}
+
+		return hashMap, nil
+	}
+
+	c, err := cc()
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		c, err := l.Accept()
+		if err != nil {
+			return
+		}
+		defer c.Close()
+
+		hashMap := map[int][]byte{}
+		buf := make([]byte, chunkSize)
+
+		for i := 0; i < times; i++ {
+			_, err := io.ReadFull(c, buf)
+			if err != nil {
+				t.Log(err.Error())
+				return
+			}
+
+			hash := md5.Sum(buf)
+			hashMap[int(buf[0])] = hash[:]
+		}
+
+		sendHash, err := writeRandData(c)
+		if err != nil {
+			t.Log(err.Error())
+			return
+		}
+
+		pingCh <- hashPair{
+			sendHash: sendHash,
+			recvHash: hashMap,
+		}
+	}()
+
+	go func() {
+		sendHash, err := writeRandData(c)
+		if err != nil {
+			t.Log(err.Error())
+			return
+		}
+
+		hashMap := map[int][]byte{}
+		buf := make([]byte, chunkSize)
+
+		for i := 0; i < times; i++ {
+			_, err := io.ReadFull(c, buf)
+			if err != nil {
+				t.Log(err.Error())
+				return
+			}
+
+			hash := md5.Sum(buf)
+			hashMap[int(buf[0])] = hash[:]
+		}
+
+		pongCh <- hashPair{
+			sendHash: sendHash,
+			recvHash: hashMap,
+		}
+	}()
+
+	return test(t)
+}
+
+func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error {
+	l, err := listenPacket("udp", ":"+F.ToString(port))
+	require.NoError(t, err)
+	defer l.Close()
+
+	rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)}
+
+	times := 50
+	chunkSize := int64(1024)
+
+	pingCh, pongCh, test := newLargeDataPair()
+	writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) {
+		hashMap := map[int][]byte{}
+		mux := sync.Mutex{}
+		for i := 0; i < times; i++ {
+			go func(idx int) {
+				buf := make([]byte, chunkSize)
+				if _, err := rand.Read(buf[1:]); err != nil {
+					t.Log(err.Error())
+					return
+				}
+				buf[0] = byte(idx)
+
+				hash := md5.Sum(buf)
+				mux.Lock()
+				hashMap[idx] = hash[:]
+				mux.Unlock()
+
+				if _, err := pc.WriteTo(buf, addr); err != nil {
+					t.Log(err.Error())
+					return
+				}
+			}(i)
+		}
+
+		return hashMap, nil
+	}
+
+	go func() {
+		var rAddr net.Addr
+		hashMap := map[int][]byte{}
+		buf := make([]byte, 64*1024)
+
+		for i := 0; i < times; i++ {
+			_, rAddr, err = l.ReadFrom(buf)
+			if err != nil {
+				t.Log(err.Error())
+				return
+			}
+
+			hash := md5.Sum(buf[:chunkSize])
+			hashMap[int(buf[0])] = hash[:]
+		}
+
+		sendHash, err := writeRandData(l, rAddr)
+		if err != nil {
+			t.Log(err.Error())
+			return
+		}
+
+		pingCh <- hashPair{
+			sendHash: sendHash,
+			recvHash: hashMap,
+		}
+	}()
+
+	pc, err := pcc()
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		sendHash, err := writeRandData(pc, rAddr)
+		if err != nil {
+			t.Log(err.Error())
+			return
+		}
+
+		hashMap := map[int][]byte{}
+		buf := make([]byte, 64*1024)
+
+		for i := 0; i < times; i++ {
+			_, _, err := pc.ReadFrom(buf)
+			if err != nil {
+				t.Log(err.Error())
+				return
+			}
+
+			hash := md5.Sum(buf[:chunkSize])
+			hashMap[int(buf[0])] = hash[:]
+		}
+
+		pongCh <- hashPair{
+			sendHash: sendHash,
+			recvHash: hashMap,
+		}
+	}()
+
+	return test(t)
+}
+
+func testPacketConnTimeout(t *testing.T, pcc func() (net.PacketConn, error)) error {
+	pc, err := pcc()
+	if err != nil {
+		return err
+	}
+
+	err = pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300))
+	require.NoError(t, err)
+
+	errCh := make(chan error, 1)
+	go func() {
+		buf := make([]byte, 1024)
+		_, _, err := pc.ReadFrom(buf)
+		errCh <- err
+	}()
+
+	select {
+	case <-errCh:
+		return nil
+	case <-time.After(time.Second * 10):
+		return errors.New("timeout")
+	}
+}
+
+func listen(network, address string) (net.Listener, error) {
+	lc := net.ListenConfig{}
+
+	var lastErr error
+	for i := 0; i < 5; i++ {
+		l, err := lc.Listen(context.Background(), network, address)
+		if err == nil {
+			return l, nil
+		}
+
+		lastErr = err
+		time.Sleep(time.Millisecond * 200)
+	}
+	return nil, lastErr
+}
+
+func listenPacket(network, address string) (net.PacketConn, error) {
+	var lastErr error
+	for i := 0; i < 5; i++ {
+		l, err := net.ListenPacket(network, address)
+		if err == nil {
+			return l, nil
+		}
+
+		lastErr = err
+		time.Sleep(time.Millisecond * 200)
+	}
+	return nil, lastErr
+}

+ 76 - 0
test/docker_test.go

@@ -0,0 +1,76 @@
+package main
+
+import (
+	"context"
+	"testing"
+
+	F "github.com/sagernet/sing/common/format"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/client"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+)
+
+type DockerOptions struct {
+	Image      string
+	EntryPoint string
+	Ports      []uint16
+	Cmd        []string
+	Env        []string
+	Bind       []string
+}
+
+func startDockerContainer(t *testing.T, options DockerOptions) {
+	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
+	require.NoError(t, err)
+	defer dockerClient.Close()
+
+	var containerOptions container.Config
+	containerOptions.Image = options.Image
+	containerOptions.Entrypoint = []string{options.EntryPoint}
+	containerOptions.Cmd = options.Cmd
+	containerOptions.Env = options.Env
+	containerOptions.ExposedPorts = make(nat.PortSet)
+
+	var hostOptions container.HostConfig
+	if !isDarwin {
+		hostOptions.NetworkMode = "host"
+	}
+	hostOptions.PortBindings = make(nat.PortMap)
+
+	for _, port := range options.Ports {
+		containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/tcp"))] = struct{}{}
+		containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/udp"))] = struct{}{}
+		hostOptions.PortBindings[nat.Port(F.ToString(port, "/tcp"))] = []nat.PortBinding{
+			{HostPort: F.ToString(port), HostIP: "0.0.0.0"},
+		}
+		hostOptions.PortBindings[nat.Port(F.ToString(port, "/udp"))] = []nat.PortBinding{
+			{HostPort: F.ToString(port), HostIP: "0.0.0.0"},
+		}
+	}
+
+	dockerContainer, err := dockerClient.ContainerCreate(context.Background(), &containerOptions, &hostOptions, nil, nil, "")
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		cleanContainer(dockerContainer.ID)
+	})
+	require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, types.ContainerStartOptions{}))
+	/*attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, types.ContainerAttachOptions{
+		Logs: true, Stream: true, Stdout: true, Stderr: true,
+	})
+	require.NoError(t, err)
+	go func() {
+		attach.Reader.WriteTo(os.Stderr)
+	}()*/
+}
+
+func cleanContainer(id string) error {
+	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
+	if err != nil {
+		return err
+	}
+	defer dockerClient.Close()
+	return dockerClient.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true})
+}

+ 42 - 0
test/go.mod

@@ -0,0 +1,42 @@
+module test
+
+go 1.18
+
+require (
+	github.com/docker/docker v20.10.17+incompatible
+	github.com/docker/go-connections v0.4.0
+	github.com/sagernet/sing v0.0.0-20220708041648-04e100e91a92
+	github.com/sagernet/sing-box v0.0.0
+	github.com/stretchr/testify v1.8.0
+	golang.org/x/net v0.0.0-20220706163947-c90051bbdb60
+)
+
+replace github.com/sagernet/sing-box => ../
+
+require (
+	github.com/Microsoft/go-winio v0.5.2 // indirect
+	github.com/database64128/tfo-go v1.0.4 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/docker/distribution v2.8.1+incompatible // indirect
+	github.com/docker/go-units v0.4.0 // indirect
+	github.com/goccy/go-json v0.9.8 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
+	github.com/kr/text v0.2.0 // indirect
+	github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
+	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
+	github.com/morikuni/aec v1.0.0 // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/image-spec v1.0.2 // indirect
+	github.com/oschwald/maxminddb-golang v1.9.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
+	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
+	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	gotest.tools/v3 v3.3.0 // indirect
+	lukechampine.com/blake3 v1.1.7 // indirect
+)

+ 118 - 0
test/go.sum

@@ -0,0 +1,118 @@
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/database64128/tfo-go v1.0.4 h1:0D9CsLor6q+2UrLhFYY3MkKkxRGf2W+27beMAo43SJc=
+github.com/database64128/tfo-go v1.0.4/go.mod h1:q5W+W0+2IHrw/Lnl0yg4sz7Kz5IDsm9x0vhwZXkRwG4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
+github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
+github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
+github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
+github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
+github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
+github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sagernet/sing v0.0.0-20220708041648-04e100e91a92 h1:c+Jg/o4UBZ+7CFdKWy8XhPN5X1rtulYdMqdgjx6PNUo=
+github.com/sagernet/sing v0.0.0-20220708041648-04e100e91a92/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
+github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+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/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=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM=
+golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
+golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
+golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
+gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
+lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
+lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=

+ 189 - 0
test/shadowsocks_test.go

@@ -0,0 +1,189 @@
+package main
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"net/netip"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	F "github.com/sagernet/sing/common/format"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestShadowsocks(t *testing.T) {
+	for _, method16 := range []string{
+		"2022-blake3-aes-128-gcm",
+	} {
+		t.Run(method16+"-inbound", func(t *testing.T) {
+			testShadowsocksInboundWithShadowsocksRust(t, method16, mkBase64(t, 16))
+		})
+		t.Run(method16+"-outbound", func(t *testing.T) {
+			testShadowsocksOutboundWithShadowsocksRust(t, method16, mkBase64(t, 16))
+		})
+		t.Run(method16+"-self", func(t *testing.T) {
+			testShadowsocksSelf(t, method16, mkBase64(t, 16))
+		})
+	}
+	for _, method32 := range []string{
+		"2022-blake3-aes-256-gcm",
+		"2022-blake3-chacha20-poly1305",
+	} {
+		t.Run(method32+"-inbound", func(t *testing.T) {
+			testShadowsocksInboundWithShadowsocksRust(t, method32, mkBase64(t, 32))
+		})
+		t.Run(method32+"-outbound", func(t *testing.T) {
+			testShadowsocksOutboundWithShadowsocksRust(t, method32, mkBase64(t, 32))
+		})
+		t.Run(method32+"-self", func(t *testing.T) {
+			testShadowsocksSelf(t, method32, mkBase64(t, 32))
+		})
+	}
+}
+
+func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, password string) {
+	t.Parallel()
+	serverPort := mkPort(t)
+	clientPort := mkPort(t)
+	testPort := mkPort(t)
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageShadowsocksRustClient,
+		EntryPoint: "sslocal",
+		Ports:      []uint16{serverPort, clientPort},
+		Cmd:        []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"},
+	})
+	startInstance(t, option.Options{
+		Log: &option.LogOption{
+			Disabled: true,
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, password string) {
+	t.Parallel()
+	serverPort := mkPort(t)
+	clientPort := mkPort(t)
+	testPort := mkPort(t)
+	startDockerContainer(t, DockerOptions{
+		Image:      ImageShadowsocksRustServer,
+		EntryPoint: "ssserver",
+		Ports:      []uint16{serverPort, testPort},
+		Cmd:        []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"},
+	})
+	startInstance(t, option.Options{
+		Log: &option.LogOption{
+			Disabled: true,
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				MixedOptions: option.SimpleInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func testShadowsocksSelf(t *testing.T, method string, password string) {
+	t.Parallel()
+	serverPort := mkPort(t)
+	clientPort := mkPort(t)
+	testPort := mkPort(t)
+	startInstance(t, option.Options{
+		Log: &option.LogOption{
+			Disabled: true,
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.SimpleInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeShadowsocks,
+				Tag:  "ss-out",
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "ss-out",
+					},
+				},
+			},
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func mkBase64(t *testing.T, length int) string {
+	psk := make([]byte, length)
+	_, err := rand.Read(psk)
+	require.NoError(t, err)
+	return base64.StdEncoding.EncodeToString(psk)
+}