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 }