condition_test.go 12 KB

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