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