From 7f8c9ffa30111a133090a470d706a4c78bc850d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 9 Jul 2022 00:01:23 +0800 Subject: [PATCH] Add shadowsocks tests --- inbound/default.go | 2 +- option/inbound.go | 2 +- test/box_test.go | 54 +++++ test/clash_darwin_test.go | 71 ++++++ test/clash_other_test.go | 12 + test/clash_test.go | 499 ++++++++++++++++++++++++++++++++++++++ test/docker_test.go | 76 ++++++ test/go.mod | 42 ++++ test/go.sum | 118 +++++++++ test/shadowsocks_test.go | 189 +++++++++++++++ 10 files changed, 1063 insertions(+), 2 deletions(-) create mode 100644 test/box_test.go create mode 100644 test/clash_darwin_test.go create mode 100644 test/clash_other_test.go create mode 100644 test/clash_test.go create mode 100644 test/docker_test.go create mode 100644 test/go.mod create mode 100644 test/go.sum create mode 100644 test/shadowsocks_test.go diff --git a/inbound/default.go b/inbound/default.go index d79e7b2c..9096a10a 100644 --- a/inbound/default.go +++ b/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 diff --git a/option/inbound.go b/option/inbound.go index 2d1877db..d3e64e08 100644 --- a/option/inbound.go +++ b/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"` diff --git a/test/box_test.go b/test/box_test.go new file mode 100644 index 00000000..757685c0 --- /dev/null +++ b/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)) +} diff --git a/test/clash_darwin_test.go b/test/clash_darwin_test.go new file mode 100644 index 00000000..d4eff13e --- /dev/null +++ b/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") +} diff --git a/test/clash_other_test.go b/test/clash_other_test.go new file mode 100644 index 00000000..fc4a68af --- /dev/null +++ b/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") +} diff --git a/test/clash_test.go b/test/clash_test.go new file mode 100644 index 00000000..76be36b7 --- /dev/null +++ b/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 +} diff --git a/test/docker_test.go b/test/docker_test.go new file mode 100644 index 00000000..45db60c3 --- /dev/null +++ b/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}) +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 00000000..98bb2ccc --- /dev/null +++ b/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 +) diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 00000000..00bb0ddc --- /dev/null +++ b/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= diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go new file mode 100644 index 00000000..ce8bbc9c --- /dev/null +++ b/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) +}