github.com/v2fly/v2ray-core/v4@v4.45.2/app/router/command/command_test.go (about)

     1  package command_test
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/golang/mock/gomock"
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/google/go-cmp/cmp/cmpopts"
    11  	"google.golang.org/grpc"
    12  	"google.golang.org/grpc/test/bufconn"
    13  
    14  	"github.com/v2fly/v2ray-core/v4/app/router"
    15  	. "github.com/v2fly/v2ray-core/v4/app/router/command"
    16  	"github.com/v2fly/v2ray-core/v4/app/stats"
    17  	"github.com/v2fly/v2ray-core/v4/common"
    18  	"github.com/v2fly/v2ray-core/v4/common/net"
    19  	"github.com/v2fly/v2ray-core/v4/features/routing"
    20  	"github.com/v2fly/v2ray-core/v4/testing/mocks"
    21  )
    22  
    23  func TestServiceSubscribeRoutingStats(t *testing.T) {
    24  	c := stats.NewChannel(&stats.ChannelConfig{
    25  		SubscriberLimit: 1,
    26  		BufferSize:      0,
    27  		Blocking:        true,
    28  	})
    29  	common.Must(c.Start())
    30  	defer c.Close()
    31  
    32  	lis := bufconn.Listen(1024 * 1024)
    33  	bufDialer := func(context.Context, string) (net.Conn, error) {
    34  		return lis.Dial()
    35  	}
    36  
    37  	testCases := []*RoutingContext{
    38  		{InboundTag: "in", OutboundTag: "out"},
    39  		{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
    40  		{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
    41  		{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
    42  		{Network: net.Network_UDP, OutboundGroupTags: []string{"outergroup", "innergroup"}, OutboundTag: "out"},
    43  		{Protocol: "bittorrent", OutboundTag: "blocked"},
    44  		{User: "example@v2fly.org", OutboundTag: "out"},
    45  		{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
    46  	}
    47  	errCh := make(chan error)
    48  	nextPub := make(chan struct{})
    49  
    50  	// Server goroutine
    51  	go func() {
    52  		server := grpc.NewServer()
    53  		RegisterRoutingServiceServer(server, NewRoutingServer(nil, c))
    54  		errCh <- server.Serve(lis)
    55  	}()
    56  
    57  	// Publisher goroutine
    58  	go func() {
    59  		publishTestCases := func() error {
    60  			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    61  			defer cancel()
    62  			for { // Wait until there's one subscriber in routing stats channel
    63  				if len(c.Subscribers()) > 0 {
    64  					break
    65  				}
    66  				if ctx.Err() != nil {
    67  					return ctx.Err()
    68  				}
    69  			}
    70  			for _, tc := range testCases {
    71  				c.Publish(context.Background(), AsRoutingRoute(tc))
    72  				time.Sleep(time.Millisecond)
    73  			}
    74  			return nil
    75  		}
    76  
    77  		if err := publishTestCases(); err != nil {
    78  			errCh <- err
    79  		}
    80  
    81  		// Wait for next round of publishing
    82  		<-nextPub
    83  
    84  		if err := publishTestCases(); err != nil {
    85  			errCh <- err
    86  		}
    87  	}()
    88  
    89  	// Client goroutine
    90  	go func() {
    91  		defer lis.Close()
    92  		conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
    93  		if err != nil {
    94  			errCh <- err
    95  			return
    96  		}
    97  		defer conn.Close()
    98  		client := NewRoutingServiceClient(conn)
    99  
   100  		// Test retrieving all fields
   101  		testRetrievingAllFields := func() error {
   102  			streamCtx, streamClose := context.WithCancel(context.Background())
   103  
   104  			// Test the unsubscription of stream works well
   105  			defer func() {
   106  				streamClose()
   107  				timeOutCtx, timeout := context.WithTimeout(context.Background(), time.Second)
   108  				defer timeout()
   109  				for { // Wait until there's no subscriber in routing stats channel
   110  					if len(c.Subscribers()) == 0 {
   111  						break
   112  					}
   113  					if timeOutCtx.Err() != nil {
   114  						t.Error("unexpected subscribers not decreased in channel", timeOutCtx.Err())
   115  					}
   116  				}
   117  			}()
   118  
   119  			stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{})
   120  			if err != nil {
   121  				return err
   122  			}
   123  
   124  			for _, tc := range testCases {
   125  				msg, err := stream.Recv()
   126  				if err != nil {
   127  					return err
   128  				}
   129  				if r := cmp.Diff(msg, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
   130  					t.Error(r)
   131  				}
   132  			}
   133  
   134  			// Test that double subscription will fail
   135  			errStream, err := client.SubscribeRoutingStats(context.Background(), &SubscribeRoutingStatsRequest{
   136  				FieldSelectors: []string{"ip", "port", "domain", "outbound"},
   137  			})
   138  			if err != nil {
   139  				return err
   140  			}
   141  			if _, err := errStream.Recv(); err == nil {
   142  				t.Error("unexpected successful subscription")
   143  			}
   144  
   145  			return nil
   146  		}
   147  
   148  		// Test retrieving only a subset of fields
   149  		testRetrievingSubsetOfFields := func() error {
   150  			streamCtx, streamClose := context.WithCancel(context.Background())
   151  			defer streamClose()
   152  			stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{
   153  				FieldSelectors: []string{"ip", "port", "domain", "outbound"},
   154  			})
   155  			if err != nil {
   156  				return err
   157  			}
   158  
   159  			// Send nextPub signal to start next round of publishing
   160  			close(nextPub)
   161  
   162  			for _, tc := range testCases {
   163  				msg, err := stream.Recv()
   164  				if err != nil {
   165  					return err
   166  				}
   167  				stat := &RoutingContext{ // Only a subset of stats is retrieved
   168  					SourceIPs:         tc.SourceIPs,
   169  					TargetIPs:         tc.TargetIPs,
   170  					SourcePort:        tc.SourcePort,
   171  					TargetPort:        tc.TargetPort,
   172  					TargetDomain:      tc.TargetDomain,
   173  					OutboundGroupTags: tc.OutboundGroupTags,
   174  					OutboundTag:       tc.OutboundTag,
   175  				}
   176  				if r := cmp.Diff(msg, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
   177  					t.Error(r)
   178  				}
   179  			}
   180  
   181  			return nil
   182  		}
   183  
   184  		if err := testRetrievingAllFields(); err != nil {
   185  			errCh <- err
   186  		}
   187  		if err := testRetrievingSubsetOfFields(); err != nil {
   188  			errCh <- err
   189  		}
   190  		errCh <- nil // Client passed all tests successfully
   191  	}()
   192  
   193  	// Wait for goroutines to complete
   194  	select {
   195  	case <-time.After(2 * time.Second):
   196  		t.Fatal("Test timeout after 2s")
   197  	case err := <-errCh:
   198  		if err != nil {
   199  			t.Fatal(err)
   200  		}
   201  	}
   202  }
   203  
   204  func TestSerivceTestRoute(t *testing.T) {
   205  	c := stats.NewChannel(&stats.ChannelConfig{
   206  		SubscriberLimit: 1,
   207  		BufferSize:      16,
   208  		Blocking:        true,
   209  	})
   210  	common.Must(c.Start())
   211  	defer c.Close()
   212  
   213  	r := new(router.Router)
   214  	mockCtl := gomock.NewController(t)
   215  	defer mockCtl.Finish()
   216  	common.Must(r.Init(context.TODO(), &router.Config{
   217  		Rule: []*router.RoutingRule{
   218  			{
   219  				InboundTag: []string{"in"},
   220  				TargetTag:  &router.RoutingRule_Tag{Tag: "out"},
   221  			},
   222  			{
   223  				Protocol:  []string{"bittorrent"},
   224  				TargetTag: &router.RoutingRule_Tag{Tag: "blocked"},
   225  			},
   226  			{
   227  				PortList:  &net.PortList{Range: []*net.PortRange{{From: 8080, To: 8080}}},
   228  				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
   229  			},
   230  			{
   231  				SourcePortList: &net.PortList{Range: []*net.PortRange{{From: 9999, To: 9999}}},
   232  				TargetTag:      &router.RoutingRule_Tag{Tag: "out"},
   233  			},
   234  			{
   235  				Domain:    []*router.Domain{{Type: router.Domain_Domain, Value: "com"}},
   236  				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
   237  			},
   238  			{
   239  				SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
   240  				TargetTag:   &router.RoutingRule_Tag{Tag: "out"},
   241  			},
   242  			{
   243  				UserEmail: []string{"example@v2fly.org"},
   244  				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
   245  			},
   246  			{
   247  				Networks:  []net.Network{net.Network_UDP, net.Network_TCP},
   248  				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
   249  			},
   250  		},
   251  	}, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl)))
   252  
   253  	lis := bufconn.Listen(1024 * 1024)
   254  	bufDialer := func(context.Context, string) (net.Conn, error) {
   255  		return lis.Dial()
   256  	}
   257  
   258  	errCh := make(chan error)
   259  
   260  	// Server goroutine
   261  	go func() {
   262  		server := grpc.NewServer()
   263  		RegisterRoutingServiceServer(server, NewRoutingServer(r, c))
   264  		errCh <- server.Serve(lis)
   265  	}()
   266  
   267  	// Client goroutine
   268  	go func() {
   269  		defer lis.Close()
   270  		conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
   271  		if err != nil {
   272  			errCh <- err
   273  		}
   274  		defer conn.Close()
   275  		client := NewRoutingServiceClient(conn)
   276  
   277  		testCases := []*RoutingContext{
   278  			{InboundTag: "in", OutboundTag: "out"},
   279  			{TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"},
   280  			{TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"},
   281  			{SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"},
   282  			{Network: net.Network_UDP, Protocol: "bittorrent", OutboundTag: "blocked"},
   283  			{User: "example@v2fly.org", OutboundTag: "out"},
   284  			{SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"},
   285  		}
   286  
   287  		// Test simple TestRoute
   288  		testSimple := func() error {
   289  			for _, tc := range testCases {
   290  				route, err := client.TestRoute(context.Background(), &TestRouteRequest{RoutingContext: tc})
   291  				if err != nil {
   292  					return err
   293  				}
   294  				if r := cmp.Diff(route, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
   295  					t.Error(r)
   296  				}
   297  			}
   298  			return nil
   299  		}
   300  
   301  		// Test TestRoute with special options
   302  		testOptions := func() error {
   303  			sub, err := c.Subscribe()
   304  			if err != nil {
   305  				return err
   306  			}
   307  			for _, tc := range testCases {
   308  				route, err := client.TestRoute(context.Background(), &TestRouteRequest{
   309  					RoutingContext: tc,
   310  					FieldSelectors: []string{"ip", "port", "domain", "outbound"},
   311  					PublishResult:  true,
   312  				})
   313  				if err != nil {
   314  					return err
   315  				}
   316  				stat := &RoutingContext{ // Only a subset of stats is retrieved
   317  					SourceIPs:         tc.SourceIPs,
   318  					TargetIPs:         tc.TargetIPs,
   319  					SourcePort:        tc.SourcePort,
   320  					TargetPort:        tc.TargetPort,
   321  					TargetDomain:      tc.TargetDomain,
   322  					OutboundGroupTags: tc.OutboundGroupTags,
   323  					OutboundTag:       tc.OutboundTag,
   324  				}
   325  				if r := cmp.Diff(route, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
   326  					t.Error(r)
   327  				}
   328  				select { // Check that routing result has been published to statistics channel
   329  				case msg, received := <-sub:
   330  					if route, ok := msg.(routing.Route); received && ok {
   331  						if r := cmp.Diff(AsProtobufMessage(nil)(route), tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" {
   332  							t.Error(r)
   333  						}
   334  					} else {
   335  						t.Error("unexpected failure in receiving published routing result for testcase", tc)
   336  					}
   337  				case <-time.After(100 * time.Millisecond):
   338  					t.Error("unexpected failure in receiving published routing result", tc)
   339  				}
   340  			}
   341  			return nil
   342  		}
   343  
   344  		if err := testSimple(); err != nil {
   345  			errCh <- err
   346  		}
   347  		if err := testOptions(); err != nil {
   348  			errCh <- err
   349  		}
   350  		errCh <- nil // Client passed all tests successfully
   351  	}()
   352  
   353  	// Wait for goroutines to complete
   354  	select {
   355  	case <-time.After(2 * time.Second):
   356  		t.Fatal("Test timeout after 2s")
   357  	case err := <-errCh:
   358  		if err != nil {
   359  			t.Fatal(err)
   360  		}
   361  	}
   362  }