github.com/v2fly/v2ray-core/v5@v5.16.2-0.20240507031116-8191faa6e095/app/router/condition_test.go (about)

     1  package router_test
     2  
     3  import (
     4  	"errors"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  
    12  	"google.golang.org/protobuf/proto"
    13  
    14  	"github.com/v2fly/v2ray-core/v5/app/router"
    15  	"github.com/v2fly/v2ray-core/v5/app/router/routercommon"
    16  	"github.com/v2fly/v2ray-core/v5/common"
    17  	"github.com/v2fly/v2ray-core/v5/common/net"
    18  	"github.com/v2fly/v2ray-core/v5/common/platform/filesystem"
    19  	"github.com/v2fly/v2ray-core/v5/common/protocol"
    20  	"github.com/v2fly/v2ray-core/v5/common/protocol/http"
    21  	"github.com/v2fly/v2ray-core/v5/common/session"
    22  	"github.com/v2fly/v2ray-core/v5/features/routing"
    23  	routing_session "github.com/v2fly/v2ray-core/v5/features/routing/session"
    24  )
    25  
    26  func init() {
    27  	const (
    28  		geoipURL   = "https://raw.githubusercontent.com/v2fly/geoip/release/geoip.dat"
    29  		geositeURL = "https://raw.githubusercontent.com/v2fly/domain-list-community/release/dlc.dat"
    30  	)
    31  
    32  	wd, err := os.Getwd()
    33  	common.Must(err)
    34  
    35  	tempPath := filepath.Join(wd, "..", "..", "testing", "temp")
    36  	geoipPath := filepath.Join(tempPath, "geoip.dat")
    37  	geositePath := filepath.Join(tempPath, "geosite.dat")
    38  
    39  	os.Setenv("v2ray.location.asset", tempPath)
    40  
    41  	if _, err := os.Stat(geoipPath); err != nil && errors.Is(err, fs.ErrNotExist) {
    42  		common.Must(os.MkdirAll(tempPath, 0o755))
    43  		geoipBytes, err := common.FetchHTTPContent(geoipURL)
    44  		common.Must(err)
    45  		common.Must(filesystem.WriteFile(geoipPath, geoipBytes))
    46  	}
    47  	if _, err := os.Stat(geositePath); err != nil && errors.Is(err, fs.ErrNotExist) {
    48  		common.Must(os.MkdirAll(tempPath, 0o755))
    49  		geositeBytes, err := common.FetchHTTPContent(geositeURL)
    50  		common.Must(err)
    51  		common.Must(filesystem.WriteFile(geositePath, geositeBytes))
    52  	}
    53  }
    54  
    55  func withBackground() routing.Context {
    56  	return &routing_session.Context{}
    57  }
    58  
    59  func withOutbound(outbound *session.Outbound) routing.Context {
    60  	return &routing_session.Context{Outbound: outbound}
    61  }
    62  
    63  func withInbound(inbound *session.Inbound) routing.Context {
    64  	return &routing_session.Context{Inbound: inbound}
    65  }
    66  
    67  func withContent(content *session.Content) routing.Context {
    68  	return &routing_session.Context{Content: content}
    69  }
    70  
    71  func TestRoutingRule(t *testing.T) {
    72  	type ruleTest struct {
    73  		input  routing.Context
    74  		output bool
    75  	}
    76  
    77  	cases := []struct {
    78  		rule *router.RoutingRule
    79  		test []ruleTest
    80  	}{
    81  		{
    82  			rule: &router.RoutingRule{
    83  				Domain: []*routercommon.Domain{
    84  					{
    85  						Value: "v2fly.org",
    86  						Type:  routercommon.Domain_Plain,
    87  					},
    88  					{
    89  						Value: "google.com",
    90  						Type:  routercommon.Domain_RootDomain,
    91  					},
    92  					{
    93  						Value: "^facebook\\.com$",
    94  						Type:  routercommon.Domain_Regex,
    95  					},
    96  				},
    97  			},
    98  			test: []ruleTest{
    99  				{
   100  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)}),
   101  					output: true,
   102  				},
   103  				{
   104  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.v2fly.org.www"), 80)}),
   105  					output: true,
   106  				},
   107  				{
   108  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.co"), 80)}),
   109  					output: false,
   110  				},
   111  				{
   112  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.google.com"), 80)}),
   113  					output: true,
   114  				},
   115  				{
   116  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("facebook.com"), 80)}),
   117  					output: true,
   118  				},
   119  				{
   120  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.facebook.com"), 80)}),
   121  					output: false,
   122  				},
   123  				{
   124  					input:  withBackground(),
   125  					output: false,
   126  				},
   127  			},
   128  		},
   129  		{
   130  			rule: &router.RoutingRule{
   131  				Cidr: []*routercommon.CIDR{
   132  					{
   133  						Ip:     []byte{8, 8, 8, 8},
   134  						Prefix: 32,
   135  					},
   136  					{
   137  						Ip:     []byte{8, 8, 8, 8},
   138  						Prefix: 32,
   139  					},
   140  					{
   141  						Ip:     net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
   142  						Prefix: 128,
   143  					},
   144  				},
   145  			},
   146  			test: []ruleTest{
   147  				{
   148  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}),
   149  					output: true,
   150  				},
   151  				{
   152  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.4.4"), 80)}),
   153  					output: false,
   154  				},
   155  				{
   156  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 80)}),
   157  					output: true,
   158  				},
   159  				{
   160  					input:  withBackground(),
   161  					output: false,
   162  				},
   163  			},
   164  		},
   165  		{
   166  			rule: &router.RoutingRule{
   167  				Geoip: []*routercommon.GeoIP{
   168  					{
   169  						Cidr: []*routercommon.CIDR{
   170  							{
   171  								Ip:     []byte{8, 8, 8, 8},
   172  								Prefix: 32,
   173  							},
   174  							{
   175  								Ip:     []byte{8, 8, 8, 8},
   176  								Prefix: 32,
   177  							},
   178  							{
   179  								Ip:     net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
   180  								Prefix: 128,
   181  							},
   182  						},
   183  					},
   184  				},
   185  			},
   186  			test: []ruleTest{
   187  				{
   188  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}),
   189  					output: true,
   190  				},
   191  				{
   192  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.4.4"), 80)}),
   193  					output: false,
   194  				},
   195  				{
   196  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 80)}),
   197  					output: true,
   198  				},
   199  				{
   200  					input:  withBackground(),
   201  					output: false,
   202  				},
   203  			},
   204  		},
   205  		{
   206  			rule: &router.RoutingRule{
   207  				SourceCidr: []*routercommon.CIDR{
   208  					{
   209  						Ip:     []byte{192, 168, 0, 0},
   210  						Prefix: 16,
   211  					},
   212  				},
   213  			},
   214  			test: []ruleTest{
   215  				{
   216  					input:  withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("192.168.0.1"), 80)}),
   217  					output: true,
   218  				},
   219  				{
   220  					input:  withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("10.0.0.1"), 80)}),
   221  					output: false,
   222  				},
   223  			},
   224  		},
   225  		{
   226  			rule: &router.RoutingRule{
   227  				UserEmail: []string{
   228  					"admin@v2fly.org",
   229  				},
   230  			},
   231  			test: []ruleTest{
   232  				{
   233  					input:  withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "admin@v2fly.org"}}),
   234  					output: true,
   235  				},
   236  				{
   237  					input:  withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "love@v2fly.org"}}),
   238  					output: false,
   239  				},
   240  				{
   241  					input:  withBackground(),
   242  					output: false,
   243  				},
   244  			},
   245  		},
   246  		{
   247  			rule: &router.RoutingRule{
   248  				Protocol: []string{"http"},
   249  			},
   250  			test: []ruleTest{
   251  				{
   252  					input:  withContent(&session.Content{Protocol: (&http.SniffHeader{}).Protocol()}),
   253  					output: true,
   254  				},
   255  			},
   256  		},
   257  		{
   258  			rule: &router.RoutingRule{
   259  				InboundTag: []string{"test", "test1"},
   260  			},
   261  			test: []ruleTest{
   262  				{
   263  					input:  withInbound(&session.Inbound{Tag: "test"}),
   264  					output: true,
   265  				},
   266  				{
   267  					input:  withInbound(&session.Inbound{Tag: "test2"}),
   268  					output: false,
   269  				},
   270  			},
   271  		},
   272  		{
   273  			rule: &router.RoutingRule{
   274  				PortList: &net.PortList{
   275  					Range: []*net.PortRange{
   276  						{From: 443, To: 443},
   277  						{From: 1000, To: 1100},
   278  					},
   279  				},
   280  			},
   281  			test: []ruleTest{
   282  				{
   283  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 443)}),
   284  					output: true,
   285  				},
   286  				{
   287  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1100)}),
   288  					output: true,
   289  				},
   290  				{
   291  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1005)}),
   292  					output: true,
   293  				},
   294  				{
   295  					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 53)}),
   296  					output: false,
   297  				},
   298  			},
   299  		},
   300  		{
   301  			rule: &router.RoutingRule{
   302  				SourcePortList: &net.PortList{
   303  					Range: []*net.PortRange{
   304  						{From: 123, To: 123},
   305  						{From: 9993, To: 9999},
   306  					},
   307  				},
   308  			},
   309  			test: []ruleTest{
   310  				{
   311  					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 123)}),
   312  					output: true,
   313  				},
   314  				{
   315  					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9999)}),
   316  					output: true,
   317  				},
   318  				{
   319  					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9994)}),
   320  					output: true,
   321  				},
   322  				{
   323  					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 53)}),
   324  					output: false,
   325  				},
   326  			},
   327  		},
   328  		{
   329  			rule: &router.RoutingRule{
   330  				Protocol:   []string{"http"},
   331  				Attributes: "attrs[':path'].startswith('/test')",
   332  			},
   333  			test: []ruleTest{
   334  				{
   335  					input:  withContent(&session.Content{Protocol: "http/1.1", Attributes: map[string]string{":path": "/test/1"}}),
   336  					output: true,
   337  				},
   338  			},
   339  		},
   340  	}
   341  
   342  	for _, test := range cases {
   343  		cond, err := test.rule.BuildCondition()
   344  		common.Must(err)
   345  
   346  		for _, subtest := range test.test {
   347  			actual := cond.Apply(subtest.input)
   348  			if actual != subtest.output {
   349  				t.Error("test case failed: ", subtest.input, " expected ", subtest.output, " but got ", actual)
   350  			}
   351  		}
   352  	}
   353  }
   354  
   355  func loadGeoSite(country string) ([]*routercommon.Domain, error) {
   356  	geositeBytes, err := filesystem.ReadAsset("geosite.dat")
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	var geositeList routercommon.GeoSiteList
   361  	if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	for _, site := range geositeList.Entry {
   366  		if strings.EqualFold(site.CountryCode, country) {
   367  			return site.Domain, nil
   368  		}
   369  	}
   370  
   371  	return nil, errors.New("country not found: " + country)
   372  }
   373  
   374  func TestChinaSites(t *testing.T) {
   375  	domains, err := loadGeoSite("CN")
   376  	common.Must(err)
   377  
   378  	matcher, err := router.NewDomainMatcher("linear", domains)
   379  	common.Must(err)
   380  	mphMatcher, err := router.NewDomainMatcher("mph", domains)
   381  	common.Must(err)
   382  
   383  	type TestCase struct {
   384  		Domain string
   385  		Output bool
   386  	}
   387  	testCases := []TestCase{
   388  		{
   389  			Domain: "163.com",
   390  			Output: true,
   391  		},
   392  		{
   393  			Domain: "163.com",
   394  			Output: true,
   395  		},
   396  		{
   397  			Domain: "164.com",
   398  			Output: false,
   399  		},
   400  		{
   401  			Domain: "164.com",
   402  			Output: false,
   403  		},
   404  	}
   405  
   406  	for i := 0; i < 1024; i++ {
   407  		testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false})
   408  	}
   409  
   410  	for _, testCase := range testCases {
   411  		r1 := matcher.Match(testCase.Domain)
   412  		r2 := mphMatcher.Match(testCase.Domain)
   413  		if r1 != testCase.Output {
   414  			t.Error("DomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r1)
   415  		} else if r2 != testCase.Output {
   416  			t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r2)
   417  		}
   418  	}
   419  }
   420  
   421  func BenchmarkMphDomainMatcher(b *testing.B) {
   422  	domains, err := loadGeoSite("CN")
   423  	common.Must(err)
   424  
   425  	matcher, err := router.NewDomainMatcher("mph", domains)
   426  	common.Must(err)
   427  
   428  	type TestCase struct {
   429  		Domain string
   430  		Output bool
   431  	}
   432  	testCases := []TestCase{
   433  		{
   434  			Domain: "163.com",
   435  			Output: true,
   436  		},
   437  		{
   438  			Domain: "163.com",
   439  			Output: true,
   440  		},
   441  		{
   442  			Domain: "164.com",
   443  			Output: false,
   444  		},
   445  		{
   446  			Domain: "164.com",
   447  			Output: false,
   448  		},
   449  	}
   450  
   451  	for i := 0; i < 1024; i++ {
   452  		testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false})
   453  	}
   454  
   455  	b.ResetTimer()
   456  	for i := 0; i < b.N; i++ {
   457  		for _, testCase := range testCases {
   458  			_ = matcher.Match(testCase.Domain)
   459  		}
   460  	}
   461  }
   462  
   463  func BenchmarkDomainMatcher(b *testing.B) {
   464  	domains, err := loadGeoSite("CN")
   465  	common.Must(err)
   466  
   467  	matcher, err := router.NewDomainMatcher("linear", domains)
   468  	common.Must(err)
   469  
   470  	type TestCase struct {
   471  		Domain string
   472  		Output bool
   473  	}
   474  	testCases := []TestCase{
   475  		{
   476  			Domain: "163.com",
   477  			Output: true,
   478  		},
   479  		{
   480  			Domain: "163.com",
   481  			Output: true,
   482  		},
   483  		{
   484  			Domain: "164.com",
   485  			Output: false,
   486  		},
   487  		{
   488  			Domain: "164.com",
   489  			Output: false,
   490  		},
   491  	}
   492  
   493  	for i := 0; i < 1024; i++ {
   494  		testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false})
   495  	}
   496  
   497  	b.ResetTimer()
   498  	for i := 0; i < b.N; i++ {
   499  		for _, testCase := range testCases {
   500  			_ = matcher.Match(testCase.Domain)
   501  		}
   502  	}
   503  }
   504  
   505  func BenchmarkMultiGeoIPMatcher(b *testing.B) {
   506  	var geoips []*routercommon.GeoIP
   507  
   508  	{
   509  		ips, err := loadGeoIP("CN")
   510  		common.Must(err)
   511  		geoips = append(geoips, &routercommon.GeoIP{
   512  			CountryCode: "CN",
   513  			Cidr:        ips,
   514  		})
   515  	}
   516  
   517  	{
   518  		ips, err := loadGeoIP("JP")
   519  		common.Must(err)
   520  		geoips = append(geoips, &routercommon.GeoIP{
   521  			CountryCode: "JP",
   522  			Cidr:        ips,
   523  		})
   524  	}
   525  
   526  	{
   527  		ips, err := loadGeoIP("CA")
   528  		common.Must(err)
   529  		geoips = append(geoips, &routercommon.GeoIP{
   530  			CountryCode: "CA",
   531  			Cidr:        ips,
   532  		})
   533  	}
   534  
   535  	{
   536  		ips, err := loadGeoIP("US")
   537  		common.Must(err)
   538  		geoips = append(geoips, &routercommon.GeoIP{
   539  			CountryCode: "US",
   540  			Cidr:        ips,
   541  		})
   542  	}
   543  
   544  	matcher, err := router.NewMultiGeoIPMatcher(geoips, false)
   545  	common.Must(err)
   546  
   547  	ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)})
   548  
   549  	b.ResetTimer()
   550  
   551  	for i := 0; i < b.N; i++ {
   552  		_ = matcher.Apply(ctx)
   553  	}
   554  }