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