From 3d806b5e4c8260a21b8dcff62d91a9d76871f875 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 17 Apr 2025 01:36:14 +0800 Subject: [PATCH] chore: add inbound test for shadowsocks/trojan --- listener/inbound/common_test.go | 35 ++--- listener/inbound/shadowsocks_test.go | 85 +++++++++++ listener/inbound/trojan_test.go | 210 +++++++++++++++++++++++++++ listener/inbound/vless_test.go | 25 ++-- 4 files changed, 324 insertions(+), 31 deletions(-) create mode 100644 listener/inbound/shadowsocks_test.go create mode 100644 listener/inbound/trojan_test.go diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index be8174191..29d045668 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -24,6 +24,9 @@ import ( "github.com/stretchr/testify/assert" ) +var httpPath = "/inbound_test" +var httpData = make([]byte, 10240) +var remoteAddr = netip.MustParseAddr("1.2.3.4") var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = N.NewRandomTLSKeyPair() var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} @@ -33,6 +36,7 @@ var realityDest = "itunes.apple.com" var realityShortid = "10f897e26c4b9478" func init() { + rand.Read(httpData) privateKey, err := generater.GeneratePrivateKey() if err != nil { panic(err) @@ -115,30 +119,17 @@ func (c *WaitCloseConn) Close() error { var _ C.Tunnel = (*TestTunnel)(nil) var _ net.Listener = (*TestTunnelListener)(nil) -type HttpTestConfig struct { - RemoteAddr netip.Addr - HttpPath string - HttpData []byte -} - func NewHttpTestTunnel() *TestTunnel { - httpData := make([]byte, 10240) - rand.Read(httpData) - config := &HttpTestConfig{ - HttpPath: "/inbound_test", - HttpData: httpData, - RemoteAddr: netip.MustParseAddr("1.2.3.4"), - } ctx, cancel := context.WithCancel(context.Background()) - ln := &TestTunnelListener{ch: make(chan net.Conn), ctx: ctx, cancel: cancel, addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(config.RemoteAddr, 0))} + ln := &TestTunnelListener{ch: make(chan net.Conn), ctx: ctx, cancel: cancel, addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(remoteAddr, 0))} r := chi.NewRouter() - r.Get(config.HttpPath, func(w http.ResponseWriter, r *http.Request) { - render.Data(w, r, config.HttpData) + r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) { + render.Data(w, r, httpData) }) go http.Serve(ln, r) testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, config.RemoteAddr, config.HttpPath), nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil) assert.NoError(t, err) req = req.WithContext(ctx) @@ -148,7 +139,7 @@ func NewHttpTestTunnel() *TestTunnel { } metadata := &C.Metadata{ NetWork: C.TCP, - DstIP: config.RemoteAddr, + DstIP: remoteAddr, DstPort: dstPort, } instance, err := proxy.DialContext(ctx, metadata) @@ -165,7 +156,7 @@ func NewHttpTestTunnel() *TestTunnel { TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, // for our self-signed cert - TLSClientConfig: tlsClientConfig, + TLSClientConfig: tlsClientConfig.Clone(), // open http2 ForceAttemptHTTP2: true, } @@ -189,12 +180,12 @@ func NewHttpTestTunnel() *TestTunnel { data, err := io.ReadAll(resp.Body) assert.NoError(t, err) - assert.Equal(t, config.HttpData, data) + assert.Equal(t, httpData, data) } tunnel := &TestTunnel{ HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) { defer conn.Close() - if metadata.DstIP != config.RemoteAddr && metadata.Host != realityDest { + if metadata.DstIP != remoteAddr && metadata.Host != realityDest { return // not match, just return } c := &WaitCloseConn{ @@ -202,7 +193,7 @@ func NewHttpTestTunnel() *TestTunnel { ch: make(chan struct{}), } if metadata.DstPort == 443 { - tlsConn := tls.Server(c, tlsConfig) + tlsConn := tls.Server(c, tlsConfig.Clone()) if metadata.Host == realityDest { // ignore the tls handshake error for realityDest ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go new file mode 100644 index 000000000..044898a34 --- /dev/null +++ b/listener/inbound/shadowsocks_test.go @@ -0,0 +1,85 @@ +package inbound_test + +import ( + "crypto/rand" + "encoding/base64" + "net/netip" + "strings" + "testing" + + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/listener/inbound" + + shadowsocks "github.com/metacubex/sing-shadowsocks" + "github.com/metacubex/sing-shadowsocks/shadowaead" + "github.com/metacubex/sing-shadowsocks/shadowaead_2022" + "github.com/metacubex/sing-shadowsocks/shadowstream" + "github.com/stretchr/testify/assert" +) + +var shadowsocksCipherList = []string{shadowsocks.MethodNone} +var shadowsocksPassword32 string +var shadowsocksPassword16 string + +func init() { + shadowsocksCipherList = append(shadowsocksCipherList, shadowaead.List...) + shadowsocksCipherList = append(shadowsocksCipherList, shadowaead_2022.List...) + shadowsocksCipherList = append(shadowsocksCipherList, shadowstream.List...) + passwordBytes := make([]byte, 32) + rand.Read(passwordBytes) + shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes) + shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) +} + +func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { + for _, cipher := range shadowsocksCipherList { + t.Run(cipher, func(t *testing.T) { + t.Parallel() + inboundOptions.Cipher = cipher + outboundOptions.Cipher = cipher + testInboundShadowSocks0(t, inboundOptions, outboundOptions) + }) + } +} + +func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { + password := shadowsocksPassword32 + if strings.Contains(inboundOptions.Cipher, "-128-") { + password = shadowsocksPassword16 + } + inboundOptions.BaseOption = inbound.BaseOption{ + NameStr: "shadowsocks_inbound", + Listen: "127.0.0.1", + Port: "0", + } + inboundOptions.Password = password + in, err := inbound.NewShadowSocks(&inboundOptions) + assert.NoError(t, err) + + tunnel := NewHttpTestTunnel() + defer tunnel.Close() + + err = in.Listen(tunnel) + assert.NoError(t, err) + defer in.Close() + + addrPort, err := netip.ParseAddrPort(in.Address()) + assert.NoError(t, err) + + outboundOptions.Name = "shadowsocks_outbound" + outboundOptions.Server = addrPort.Addr().String() + outboundOptions.Port = int(addrPort.Port()) + outboundOptions.Password = password + + out, err := outbound.NewShadowSocks(outboundOptions) + assert.NoError(t, err) + defer out.Close() + + tunnel.DoTest(t, out) +} + +func TestInboundShadowSocks_Basic(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{} + outboundOptions := outbound.ShadowSocksOption{} + testInboundShadowSocks(t, inboundOptions, outboundOptions) +} diff --git a/listener/inbound/trojan_test.go b/listener/inbound/trojan_test.go new file mode 100644 index 000000000..ad35b8819 --- /dev/null +++ b/listener/inbound/trojan_test.go @@ -0,0 +1,210 @@ +package inbound_test + +import ( + "net" + "net/netip" + "testing" + + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/listener/inbound" + "github.com/stretchr/testify/assert" +) + +func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { + userUUID := utils.NewUUIDV4().String() + inboundOptions.BaseOption = inbound.BaseOption{ + NameStr: "trojan_inbound", + Listen: "127.0.0.1", + Port: "0", + } + inboundOptions.Users = []inbound.TrojanUser{ + {Username: "test", Password: userUUID}, + } + in, err := inbound.NewTrojan(&inboundOptions) + assert.NoError(t, err) + + tunnel := NewHttpTestTunnel() + defer tunnel.Close() + + err = in.Listen(tunnel) + assert.NoError(t, err) + defer in.Close() + + addrPort, err := netip.ParseAddrPort(in.Address()) + assert.NoError(t, err) + + outboundOptions.Name = "trojan_outbound" + outboundOptions.Server = addrPort.Addr().String() + outboundOptions.Port = int(addrPort.Port()) + outboundOptions.Password = userUUID + + out, err := outbound.NewTrojan(outboundOptions) + assert.NoError(t, err) + defer out.Close() + + tunnel.DoTest(t, out) +} + +func TestInboundTrojan_Tls(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Wss1(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + WsPath: "/ws", + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + Network: "ws", + WSOpts: outbound.WSOptions{ + Path: "/ws", + }, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Wss2(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + WsPath: "/ws", + GrpcServiceName: "GunService", + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + Network: "ws", + WSOpts: outbound.WSOptions{ + Path: "/ws", + }, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Grpc1(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + GrpcServiceName: "GunService", + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + Network: "grpc", + GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Grpc2(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + WsPath: "/ws", + GrpcServiceName: "GunService", + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + Network: "grpc", + GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Reality(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + RealityConfig: inbound.RealityConfig{ + Dest: net.JoinHostPort(realityDest, "443"), + PrivateKey: realityPrivateKey, + ShortID: []string{realityShortid}, + ServerNames: []string{realityDest}, + }, + } + outboundOptions := outbound.TrojanOption{ + SNI: realityDest, + RealityOpts: outbound.RealityOptions{ + PublicKey: realityPublickey, + ShortID: realityShortid, + }, + ClientFingerprint: "chrome", + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Reality_Grpc(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + RealityConfig: inbound.RealityConfig{ + Dest: net.JoinHostPort(realityDest, "443"), + PrivateKey: realityPrivateKey, + ShortID: []string{realityShortid}, + ServerNames: []string{realityDest}, + }, + GrpcServiceName: "GunService", + } + outboundOptions := outbound.TrojanOption{ + SNI: realityDest, + RealityOpts: outbound.RealityOptions{ + PublicKey: realityPublickey, + ShortID: realityShortid, + }, + ClientFingerprint: "chrome", + Network: "grpc", + GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Tls_TrojanSS(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + SSOption: inbound.TrojanSSOption{ + Enabled: true, + Method: "", + Password: "password", + }, + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + SSOpts: outbound.TrojanSSOption{ + Enabled: true, + Method: "", + Password: "password", + }, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} + +func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + SSOption: inbound.TrojanSSOption{ + Enabled: true, + Method: "", + Password: "password", + }, + WsPath: "/ws", + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + SSOpts: outbound.TrojanSSOption{ + Enabled: true, + Method: "", + Password: "password", + }, + Network: "ws", + WSOpts: outbound.WSOptions{ + Path: "/ws", + }, + } + testInboundTrojan(t, inboundOptions, outboundOptions) +} diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index 84811f390..ed7a50734 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -56,8 +56,10 @@ func TestInboundVless_Tls(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundVless(t, inboundOptions, outboundOptions) - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Wss1(t *testing.T) { @@ -73,11 +75,12 @@ func TestInboundVless_Wss1(t *testing.T) { WSOpts: outbound.WSOptions{ Path: "/ws", }, - ClientFingerprint: "chrome", } testInboundVless(t, inboundOptions, outboundOptions) - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Wss2(t *testing.T) { @@ -96,8 +99,10 @@ func TestInboundVless_Wss2(t *testing.T) { }, } testInboundVless(t, inboundOptions, outboundOptions) - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Grpc1(t *testing.T) { @@ -150,8 +155,10 @@ func TestInboundVless_Reality(t *testing.T) { ClientFingerprint: "chrome", } testInboundVless(t, inboundOptions, outboundOptions) - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Reality_Grpc(t *testing.T) {