mirror of
				https://github.com/optim-enterprises-bv/Xray-core.git
				synced 2025-10-30 18:18:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			362 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package command_test
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/golang/mock/gomock"
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"github.com/google/go-cmp/cmp/cmpopts"
 | |
| 	"github.com/xtls/xray-core/v1/app/router"
 | |
| 	. "github.com/xtls/xray-core/v1/app/router/command"
 | |
| 	"github.com/xtls/xray-core/v1/app/stats"
 | |
| 	"github.com/xtls/xray-core/v1/common"
 | |
| 	"github.com/xtls/xray-core/v1/common/net"
 | |
| 	"github.com/xtls/xray-core/v1/features/routing"
 | |
| 	"github.com/xtls/xray-core/v1/testing/mocks"
 | |
| 	"google.golang.org/grpc"
 | |
| 	"google.golang.org/grpc/test/bufconn"
 | |
| )
 | |
| 
 | |
| func TestServiceSubscribeRoutingStats(t *testing.T) {
 | |
| 	c := stats.NewChannel(&stats.ChannelConfig{
 | |
| 		SubscriberLimit: 1,
 | |
| 		BufferSize:      0,
 | |
| 		Blocking:        true,
 | |
| 	})
 | |
| 	common.Must(c.Start())
 | |
| 	defer c.Close()
 | |
| 
 | |
| 	lis := bufconn.Listen(1024 * 1024)
 | |
| 	bufDialer := func(context.Context, string) (net.Conn, error) {
 | |
| 		return lis.Dial()
 | |
| 	}
 | |
| 
 | |
| 	testCases := []*RoutingContext{
 | |
| 		{InboundTag: "in", OutboundTag: "out"},
 | |
| 		{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
 | |
| 		{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
 | |
| 		{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
 | |
| 		{Network: net.Network_UDP, OutboundGroupTags: []string{"outergroup", "innergroup"}, OutboundTag: "out"},
 | |
| 		{Protocol: "bittorrent", OutboundTag: "blocked"},
 | |
| 		{User: "example@example.com", OutboundTag: "out"},
 | |
| 		{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
 | |
| 	}
 | |
| 	errCh := make(chan error)
 | |
| 	nextPub := make(chan struct{})
 | |
| 
 | |
| 	// Server goroutine
 | |
| 	go func() {
 | |
| 		server := grpc.NewServer()
 | |
| 		RegisterRoutingServiceServer(server, NewRoutingServer(nil, c))
 | |
| 		errCh <- server.Serve(lis)
 | |
| 	}()
 | |
| 
 | |
| 	// Publisher goroutine
 | |
| 	go func() {
 | |
| 		publishTestCases := func() error {
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 | |
| 			defer cancel()
 | |
| 			for { // Wait until there's one subscriber in routing stats channel
 | |
| 				if len(c.Subscribers()) > 0 {
 | |
| 					break
 | |
| 				}
 | |
| 				if ctx.Err() != nil {
 | |
| 					return ctx.Err()
 | |
| 				}
 | |
| 			}
 | |
| 			for _, tc := range testCases {
 | |
| 				c.Publish(context.Background(), AsRoutingRoute(tc))
 | |
| 				time.Sleep(time.Millisecond)
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		if err := publishTestCases(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 
 | |
| 		// Wait for next round of publishing
 | |
| 		<-nextPub
 | |
| 
 | |
| 		if err := publishTestCases(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// Client goroutine
 | |
| 	go func() {
 | |
| 		defer lis.Close()
 | |
| 		conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
 | |
| 		if err != nil {
 | |
| 			errCh <- err
 | |
| 			return
 | |
| 		}
 | |
| 		defer conn.Close()
 | |
| 		client := NewRoutingServiceClient(conn)
 | |
| 
 | |
| 		// Test retrieving all fields
 | |
| 		testRetrievingAllFields := func() error {
 | |
| 			streamCtx, streamClose := context.WithCancel(context.Background())
 | |
| 
 | |
| 			// Test the unsubscription of stream works well
 | |
| 			defer func() {
 | |
| 				streamClose()
 | |
| 				timeOutCtx, timeout := context.WithTimeout(context.Background(), time.Second)
 | |
| 				defer timeout()
 | |
| 				for { // Wait until there's no subscriber in routing stats channel
 | |
| 					if len(c.Subscribers()) == 0 {
 | |
| 						break
 | |
| 					}
 | |
| 					if timeOutCtx.Err() != nil {
 | |
| 						t.Error("unexpected subscribers not decreased in channel", timeOutCtx.Err())
 | |
| 					}
 | |
| 				}
 | |
| 			}()
 | |
| 
 | |
| 			stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			for _, tc := range testCases {
 | |
| 				msg, err := stream.Recv()
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				if r := cmp.Diff(msg, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
 | |
| 					t.Error(r)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// Test that double subscription will fail
 | |
| 			errStream, err := client.SubscribeRoutingStats(context.Background(), &SubscribeRoutingStatsRequest{
 | |
| 				FieldSelectors: []string{"ip", "port", "domain", "outbound"},
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			if _, err := errStream.Recv(); err == nil {
 | |
| 				t.Error("unexpected successful subscription")
 | |
| 			}
 | |
| 
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		// Test retrieving only a subset of fields
 | |
| 		testRetrievingSubsetOfFields := func() error {
 | |
| 			streamCtx, streamClose := context.WithCancel(context.Background())
 | |
| 			defer streamClose()
 | |
| 			stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{
 | |
| 				FieldSelectors: []string{"ip", "port", "domain", "outbound"},
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			// Send nextPub signal to start next round of publishing
 | |
| 			close(nextPub)
 | |
| 
 | |
| 			for _, tc := range testCases {
 | |
| 				msg, err := stream.Recv()
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				stat := &RoutingContext{ // Only a subset of stats is retrieved
 | |
| 					SourceIPs:         tc.SourceIPs,
 | |
| 					TargetIPs:         tc.TargetIPs,
 | |
| 					SourcePort:        tc.SourcePort,
 | |
| 					TargetPort:        tc.TargetPort,
 | |
| 					TargetDomain:      tc.TargetDomain,
 | |
| 					OutboundGroupTags: tc.OutboundGroupTags,
 | |
| 					OutboundTag:       tc.OutboundTag,
 | |
| 				}
 | |
| 				if r := cmp.Diff(msg, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
 | |
| 					t.Error(r)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		if err := testRetrievingAllFields(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 		if err := testRetrievingSubsetOfFields(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 		errCh <- nil // Client passed all tests successfully
 | |
| 	}()
 | |
| 
 | |
| 	// Wait for goroutines to complete
 | |
| 	select {
 | |
| 	case <-time.After(2 * time.Second):
 | |
| 		t.Fatal("Test timeout after 2s")
 | |
| 	case err := <-errCh:
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestSerivceTestRoute(t *testing.T) {
 | |
| 	c := stats.NewChannel(&stats.ChannelConfig{
 | |
| 		SubscriberLimit: 1,
 | |
| 		BufferSize:      16,
 | |
| 		Blocking:        true,
 | |
| 	})
 | |
| 	common.Must(c.Start())
 | |
| 	defer c.Close()
 | |
| 
 | |
| 	r := new(router.Router)
 | |
| 	mockCtl := gomock.NewController(t)
 | |
| 	defer mockCtl.Finish()
 | |
| 	common.Must(r.Init(&router.Config{
 | |
| 		Rule: []*router.RoutingRule{
 | |
| 			{
 | |
| 				InboundTag: []string{"in"},
 | |
| 				TargetTag:  &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				Protocol:  []string{"bittorrent"},
 | |
| 				TargetTag: &router.RoutingRule_Tag{Tag: "blocked"},
 | |
| 			},
 | |
| 			{
 | |
| 				PortList:  &net.PortList{Range: []*net.PortRange{{From: 8080, To: 8080}}},
 | |
| 				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				SourcePortList: &net.PortList{Range: []*net.PortRange{{From: 9999, To: 9999}}},
 | |
| 				TargetTag:      &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				Domain:    []*router.Domain{{Type: router.Domain_Domain, Value: "com"}},
 | |
| 				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
 | |
| 				TargetTag:   &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				UserEmail: []string{"example@example.com"},
 | |
| 				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 			{
 | |
| 				Networks:  []net.Network{net.Network_UDP, net.Network_TCP},
 | |
| 				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
 | |
| 			},
 | |
| 		},
 | |
| 	}, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl)))
 | |
| 
 | |
| 	lis := bufconn.Listen(1024 * 1024)
 | |
| 	bufDialer := func(context.Context, string) (net.Conn, error) {
 | |
| 		return lis.Dial()
 | |
| 	}
 | |
| 
 | |
| 	errCh := make(chan error)
 | |
| 
 | |
| 	// Server goroutine
 | |
| 	go func() {
 | |
| 		server := grpc.NewServer()
 | |
| 		RegisterRoutingServiceServer(server, NewRoutingServer(r, c))
 | |
| 		errCh <- server.Serve(lis)
 | |
| 	}()
 | |
| 
 | |
| 	// Client goroutine
 | |
| 	go func() {
 | |
| 		defer lis.Close()
 | |
| 		conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
 | |
| 		if err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 		defer conn.Close()
 | |
| 		client := NewRoutingServiceClient(conn)
 | |
| 
 | |
| 		testCases := []*RoutingContext{
 | |
| 			{InboundTag: "in", OutboundTag: "out"},
 | |
| 			{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
 | |
| 			{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
 | |
| 			{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
 | |
| 			{Network: net.Network_UDP, Protocol: "bittorrent", OutboundTag: "blocked"},
 | |
| 			{User: "example@example.com", OutboundTag: "out"},
 | |
| 			{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
 | |
| 		}
 | |
| 
 | |
| 		// Test simple TestRoute
 | |
| 		testSimple := func() error {
 | |
| 			for _, tc := range testCases {
 | |
| 				route, err := client.TestRoute(context.Background(), &TestRouteRequest{RoutingContext: tc})
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				if r := cmp.Diff(route, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
 | |
| 					t.Error(r)
 | |
| 				}
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		// Test TestRoute with special options
 | |
| 		testOptions := func() error {
 | |
| 			sub, err := c.Subscribe()
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			for _, tc := range testCases {
 | |
| 				route, err := client.TestRoute(context.Background(), &TestRouteRequest{
 | |
| 					RoutingContext: tc,
 | |
| 					FieldSelectors: []string{"ip", "port", "domain", "outbound"},
 | |
| 					PublishResult:  true,
 | |
| 				})
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				stat := &RoutingContext{ // Only a subset of stats is retrieved
 | |
| 					SourceIPs:         tc.SourceIPs,
 | |
| 					TargetIPs:         tc.TargetIPs,
 | |
| 					SourcePort:        tc.SourcePort,
 | |
| 					TargetPort:        tc.TargetPort,
 | |
| 					TargetDomain:      tc.TargetDomain,
 | |
| 					OutboundGroupTags: tc.OutboundGroupTags,
 | |
| 					OutboundTag:       tc.OutboundTag,
 | |
| 				}
 | |
| 				if r := cmp.Diff(route, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
 | |
| 					t.Error(r)
 | |
| 				}
 | |
| 				select { // Check that routing result has been published to statistics channel
 | |
| 				case msg, received := <-sub:
 | |
| 					if route, ok := msg.(routing.Route); received && ok {
 | |
| 						if r := cmp.Diff(AsProtobufMessage(nil)(route), tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
 | |
| 							t.Error(r)
 | |
| 						}
 | |
| 					} else {
 | |
| 						t.Error("unexpected failure in receiving published routing result for testcase", tc)
 | |
| 					}
 | |
| 				case <-time.After(100 * time.Millisecond):
 | |
| 					t.Error("unexpected failure in receiving published routing result", tc)
 | |
| 				}
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		if err := testSimple(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 		if err := testOptions(); err != nil {
 | |
| 			errCh <- err
 | |
| 		}
 | |
| 		errCh <- nil // Client passed all tests successfully
 | |
| 	}()
 | |
| 
 | |
| 	// Wait for goroutines to complete
 | |
| 	select {
 | |
| 	case <-time.After(2 * time.Second):
 | |
| 		t.Fatal("Test timeout after 2s")
 | |
| 	case err := <-errCh:
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	}
 | |
| }
 | 
