remote_routing_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. package sub
  2. import (
  3. "encoding/base64"
  4. "errors"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "sync/atomic"
  12. "testing"
  13. "time"
  14. "github.com/gin-gonic/gin"
  15. yaml "github.com/goccy/go-yaml"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database"
  17. )
  18. func mergeRemoteClashRulesYAML(base map[string]any, raw string) error {
  19. var remote map[string]any
  20. if err := yaml.Unmarshal([]byte(strings.TrimSpace(raw)), &remote); err != nil {
  21. return err
  22. }
  23. return mergeRemoteClashRules(base, remote)
  24. }
  25. type remoteRoutingRoundTripper func(*http.Request) (*http.Response, error)
  26. func (fn remoteRoutingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
  27. return fn(req)
  28. }
  29. func remoteRoutingTestClient(fn remoteRoutingRoundTripper) *http.Client {
  30. return &http.Client{Transport: fn}
  31. }
  32. func remoteRoutingResponse(status int, body string) *http.Response {
  33. return &http.Response{
  34. StatusCode: status,
  35. Header: make(http.Header),
  36. Body: io.NopCloser(strings.NewReader(body)),
  37. }
  38. }
  39. func waitRemoteRoutingIdle(t *testing.T, resolver *remoteRoutingResolver) {
  40. t.Helper()
  41. deadline := time.Now().Add(2 * time.Second)
  42. for {
  43. resolver.mu.Lock()
  44. inflight := len(resolver.inflight)
  45. resolver.mu.Unlock()
  46. if inflight == 0 {
  47. return
  48. }
  49. if time.Now().After(deadline) {
  50. t.Fatal("remote routing refresh did not finish")
  51. }
  52. time.Sleep(time.Millisecond)
  53. }
  54. }
  55. func waitRemoteRoutingLoadIdle(t *testing.T, resolver *remoteRoutingResolver) {
  56. t.Helper()
  57. deadline := time.Now().Add(2 * time.Second)
  58. for {
  59. resolver.mu.Lock()
  60. loading := resolver.loadInFlight
  61. resolver.mu.Unlock()
  62. if !loading {
  63. return
  64. }
  65. if time.Now().After(deadline) {
  66. t.Fatal("persisted routing cache load did not finish")
  67. }
  68. time.Sleep(time.Millisecond)
  69. }
  70. }
  71. func primeRemoteRouting(t *testing.T, resolver *remoteRoutingResolver, kind remoteRoutingKind, source string) string {
  72. t.Helper()
  73. if err := resolver.refreshSource(kind, source); err != nil {
  74. t.Fatalf("prime remote routing: %v", err)
  75. }
  76. value, remote, err := resolver.resolve(kind, source)
  77. if err != nil || !remote || value == "" {
  78. t.Fatalf("primed resolve got=%q remote=%v err=%v", value, remote, err)
  79. }
  80. return value
  81. }
  82. func TestNormalizeHappRoutingAcceptsJSONAndDeeplink(t *testing.T) {
  83. deeplink, err := normalizeHappRouting([]byte(`{"Name":"RoscomVPN","GlobalProxy":"true"}`))
  84. if err != nil {
  85. t.Fatalf("normalize JSON: %v", err)
  86. }
  87. const prefix = "happ://routing/onadd/"
  88. if !strings.HasPrefix(deeplink, prefix) {
  89. t.Fatalf("deeplink = %q", deeplink)
  90. }
  91. decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(deeplink, prefix))
  92. if err != nil || !strings.Contains(string(decoded), `"Name":"RoscomVPN"`) {
  93. t.Fatalf("decoded payload = %q, err=%v", decoded, err)
  94. }
  95. if got, err := normalizeHappRouting([]byte(deeplink + "\n")); err != nil || got != deeplink {
  96. t.Fatalf("ready deeplink got=%q err=%v", got, err)
  97. }
  98. if _, err := normalizeHappRouting([]byte("routing.help")); err == nil {
  99. t.Fatal("invalid Happ response was accepted")
  100. }
  101. }
  102. func TestRemoteRoutingResolverAcceptsHappRedirect(t *testing.T) {
  103. deeplink, err := normalizeHappRouting([]byte(`{"Name":"redirected"}`))
  104. if err != nil {
  105. t.Fatalf("normalize: %v", err)
  106. }
  107. var requests atomic.Int32
  108. client := remoteRoutingTestClient(func(req *http.Request) (*http.Response, error) {
  109. requests.Add(1)
  110. response := remoteRoutingResponse(http.StatusFound, "")
  111. response.Header.Set("Location", deeplink)
  112. response.Request = req
  113. return response, nil
  114. })
  115. client.CheckRedirect = checkRemoteRoutingRedirect
  116. resolver := newRemoteRoutingResolver(client, false)
  117. const source = "https://routing.example/"
  118. if err := resolver.refreshSource(remoteRoutingHapp, source); err != nil {
  119. t.Fatalf("refresh redirect: %v", err)
  120. }
  121. got, remote, err := resolver.resolve(remoteRoutingHapp, source)
  122. if err != nil || !remote || got != deeplink {
  123. t.Fatalf("redirect resolve got=%q remote=%v err=%v", got, remote, err)
  124. }
  125. if requests.Load() != 1 {
  126. t.Fatalf("network requests = %d, want 1", requests.Load())
  127. }
  128. }
  129. func TestRemoteRoutingResolverHandlesHappNotModified(t *testing.T) {
  130. var requests atomic.Int32
  131. client := remoteRoutingTestClient(func(req *http.Request) (*http.Response, error) {
  132. if requests.Add(1) == 1 {
  133. response := remoteRoutingResponse(http.StatusOK, `{"Name":"etagged"}`)
  134. response.Header.Set("ETag", `"v1"`)
  135. return response, nil
  136. }
  137. if req.Header.Get("If-None-Match") != `"v1"` {
  138. t.Errorf("If-None-Match = %q", req.Header.Get("If-None-Match"))
  139. }
  140. return remoteRoutingResponse(http.StatusNotModified, ""), nil
  141. })
  142. resolver := newRemoteRoutingResolver(client, false)
  143. now := time.Unix(1_800_000_000, 0)
  144. resolver.now = func() time.Time { return now }
  145. const source = "https://example.com/default.json"
  146. first := primeRemoteRouting(t, resolver, remoteRoutingHapp, source)
  147. now = now.Add(remoteRoutingCacheTTL + time.Second)
  148. second, _, err := resolver.resolve(remoteRoutingHapp, source)
  149. if err != nil || second != first {
  150. t.Fatalf("stale resolve got=%q err=%v", second, err)
  151. }
  152. waitRemoteRoutingIdle(t, resolver)
  153. now = now.Add(time.Minute)
  154. third, _, err := resolver.resolve(remoteRoutingHapp, source)
  155. if err != nil || third != first {
  156. t.Fatalf("refreshed cache got=%q err=%v", third, err)
  157. }
  158. if requests.Load() != 2 {
  159. t.Fatalf("requests = %d, want 2", requests.Load())
  160. }
  161. }
  162. func TestRemoteRoutingResolverDoesNotBlockAndCoalescesColdFetch(t *testing.T) {
  163. var requests atomic.Int32
  164. started := make(chan struct{})
  165. release := make(chan struct{})
  166. var startOnce sync.Once
  167. client := remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  168. requests.Add(1)
  169. startOnce.Do(func() { close(started) })
  170. <-release
  171. return remoteRoutingResponse(http.StatusOK, `{"Name":"RoscomVPN"}`), nil
  172. })
  173. resolver := newRemoteRoutingResolver(client, false)
  174. const source = "https://example.com/default.json"
  175. results := make(chan error, 8)
  176. for range 8 {
  177. go func() {
  178. _, remote, err := resolver.resolve(remoteRoutingHapp, source)
  179. if !remote {
  180. results <- errors.New("source was not classified as remote")
  181. return
  182. }
  183. results <- err
  184. }()
  185. }
  186. <-started
  187. for range 8 {
  188. select {
  189. case err := <-results:
  190. if !errors.Is(err, errRemoteRoutingUnavailable) {
  191. t.Fatalf("cold resolve err=%v", err)
  192. }
  193. case <-time.After(100 * time.Millisecond):
  194. t.Fatal("cold resolve blocked on the remote fetch")
  195. }
  196. }
  197. if got := requests.Load(); got != 1 {
  198. t.Fatalf("requests = %d, want 1", got)
  199. }
  200. close(release)
  201. waitRemoteRoutingIdle(t, resolver)
  202. if got, _, err := resolver.resolve(remoteRoutingHapp, source); err != nil || !strings.HasPrefix(got, "happ://routing/onadd/") {
  203. t.Fatalf("cached resolve got=%q err=%v", got, err)
  204. }
  205. if got := requests.Load(); got != 1 {
  206. t.Fatalf("cached request count = %d, want 1", got)
  207. }
  208. }
  209. func TestRemoteRoutingResolverServesStaleAfterFailedRefresh(t *testing.T) {
  210. var requests atomic.Int32
  211. refreshStarted := make(chan struct{})
  212. releaseRefresh := make(chan struct{})
  213. var startOnce sync.Once
  214. fail := atomic.Bool{}
  215. client := remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  216. requests.Add(1)
  217. if fail.Load() {
  218. startOnce.Do(func() { close(refreshStarted) })
  219. <-releaseRefresh
  220. return remoteRoutingResponse(http.StatusBadGateway, "bad gateway"), nil
  221. }
  222. return remoteRoutingResponse(http.StatusOK, `{"Name":"last-good"}`), nil
  223. })
  224. resolver := newRemoteRoutingResolver(client, false)
  225. now := time.Unix(1_800_000_000, 0)
  226. resolver.now = func() time.Time { return now }
  227. const source = "https://example.com/default.json"
  228. first := primeRemoteRouting(t, resolver, remoteRoutingHapp, source)
  229. fail.Store(true)
  230. now = now.Add(remoteRoutingCacheTTL + time.Second)
  231. startedAt := time.Now()
  232. stale, remote, err := resolver.resolve(remoteRoutingHapp, source)
  233. if err != nil || !remote || stale != first {
  234. t.Fatalf("stale resolve got=%q remote=%v err=%v", stale, remote, err)
  235. }
  236. if elapsed := time.Since(startedAt); elapsed > 100*time.Millisecond {
  237. t.Fatalf("stale resolve blocked for %v", elapsed)
  238. }
  239. select {
  240. case <-refreshStarted:
  241. case <-time.After(time.Second):
  242. t.Fatal("refresh did not run")
  243. }
  244. close(releaseRefresh)
  245. waitRemoteRoutingIdle(t, resolver)
  246. if got, _, err := resolver.resolve(remoteRoutingHapp, source); err != nil || got != first {
  247. t.Fatalf("negative-cache resolve got=%q err=%v", got, err)
  248. }
  249. if got := requests.Load(); got != 2 {
  250. t.Fatalf("requests = %d, want 2", got)
  251. }
  252. }
  253. func TestRemoteRoutingResolverLoadsPersistedLastGood(t *testing.T) {
  254. initSubDB(t)
  255. deeplink, err := normalizeHappRouting([]byte(`{"Name":"persisted"}`))
  256. if err != nil {
  257. t.Fatalf("normalize: %v", err)
  258. }
  259. const source = "https://example.com/default.json"
  260. entry := remoteRoutingCacheEntry{
  261. Source: source, Content: deeplink, FetchedAt: time.Now().Add(-time.Hour).Unix(), ETag: `"v1"`,
  262. }
  263. newRemoteRoutingResolver(nil, false).persistEntry(remoteRoutingHapp, entry)
  264. resolver := newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  265. return remoteRoutingResponse(http.StatusServiceUnavailable, "offline"), nil
  266. }), true)
  267. resolver.ensurePersistedLoaded()
  268. got, remote, err := resolver.resolve(remoteRoutingHapp, source)
  269. if err != nil || !remote || got != deeplink {
  270. t.Fatalf("persisted resolve got=%q remote=%v err=%v", got, remote, err)
  271. }
  272. waitRemoteRoutingIdle(t, resolver)
  273. }
  274. func TestRemoteRoutingResolverDoesNotBlockOnPersistedLoad(t *testing.T) {
  275. started := make(chan struct{})
  276. release := make(chan struct{})
  277. var startOnce sync.Once
  278. resolver := newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  279. startOnce.Do(func() { close(started) })
  280. <-release
  281. return remoteRoutingResponse(http.StatusServiceUnavailable, "offline"), nil
  282. }), true)
  283. resolver.loadMu.Lock()
  284. loadLocked := true
  285. t.Cleanup(func() {
  286. if loadLocked {
  287. resolver.loadMu.Unlock()
  288. }
  289. })
  290. startedAt := time.Now()
  291. _, remote, err := resolver.resolve(remoteRoutingHapp, "https://example.com/default.json")
  292. if !remote || !errors.Is(err, errRemoteRoutingUnavailable) {
  293. t.Fatalf("resolve remote=%v err=%v", remote, err)
  294. }
  295. if elapsed := time.Since(startedAt); elapsed > 100*time.Millisecond {
  296. t.Fatalf("resolve blocked on persisted cache load for %v", elapsed)
  297. }
  298. resolver.loadMu.Unlock()
  299. loadLocked = false
  300. close(release)
  301. select {
  302. case <-started:
  303. case <-time.After(time.Second):
  304. t.Fatal("background refresh did not start")
  305. }
  306. waitRemoteRoutingIdle(t, resolver)
  307. waitRemoteRoutingLoadIdle(t, resolver)
  308. }
  309. func TestRemoteRoutingResolverRejectsOversizedPersistedHappValue(t *testing.T) {
  310. initSubDB(t)
  311. deeplink, err := normalizeHappRouting([]byte(`{"Name":"` + strings.Repeat("x", remoteRoutingHappMaxValue) + `"}`))
  312. if err != nil || len(deeplink) <= remoteRoutingHappMaxValue {
  313. t.Fatalf("oversized fixture length=%d err=%v", len(deeplink), err)
  314. }
  315. const source = "https://example.com/oversized.json"
  316. newRemoteRoutingResolver(nil, false).persistEntry(remoteRoutingHapp, remoteRoutingCacheEntry{
  317. Source: source, Content: deeplink, FetchedAt: time.Now().Unix(),
  318. })
  319. resolver := newRemoteRoutingResolver(nil, true)
  320. resolver.ensurePersistedLoaded()
  321. resolver.mu.Lock()
  322. _, exists := resolver.entries[remoteRoutingKey{kind: remoteRoutingHapp, source: source}]
  323. resolver.mu.Unlock()
  324. if exists {
  325. t.Fatal("oversized persisted Happ routing value was loaded")
  326. }
  327. }
  328. func TestRemoteRoutingResolverDoesNotReplaceClashCacheWithInvalidSchema(t *testing.T) {
  329. var requests atomic.Int32
  330. client := remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  331. if requests.Add(1) == 1 {
  332. return remoteRoutingResponse(http.StatusOK, "rules:\n - MATCH,PROXY\n"), nil
  333. }
  334. return remoteRoutingResponse(http.StatusOK, "rules: not-a-list\n"), nil
  335. })
  336. resolver := newRemoteRoutingResolver(client, false)
  337. now := time.Unix(1_800_000_000, 0)
  338. resolver.now = func() time.Time { return now }
  339. const source = "https://example.com/routing.yaml"
  340. first := primeRemoteRouting(t, resolver, remoteRoutingClash, source)
  341. now = now.Add(remoteRoutingCacheTTL + time.Second)
  342. second, _, err := resolver.resolve(remoteRoutingClash, source)
  343. if err != nil || second != first {
  344. t.Fatalf("invalid refresh replaced last-good: got=%q err=%v", second, err)
  345. }
  346. waitRemoteRoutingIdle(t, resolver)
  347. second, _, err = resolver.resolve(remoteRoutingClash, source)
  348. if err != nil || second != first {
  349. t.Fatalf("invalid refresh replaced last-good after completion: got=%q err=%v", second, err)
  350. }
  351. if requests.Load() != 2 {
  352. t.Fatalf("requests = %d, want 2", requests.Load())
  353. }
  354. }
  355. func TestApplyCommonHeadersResolvesRemoteHappAndFailsClosed(t *testing.T) {
  356. gin.SetMode(gin.TestMode)
  357. oldResolver := routingSourceResolver
  358. t.Cleanup(func() { routingSourceResolver = oldResolver })
  359. routingSourceResolver = newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  360. return remoteRoutingResponse(http.StatusOK, `{"Name":"RoscomVPN"}`), nil
  361. }), false)
  362. const source = "https://example.com/default.json"
  363. primeRemoteRouting(t, routingSourceResolver, remoteRoutingHapp, source)
  364. recorder := httptest.NewRecorder()
  365. ctx, _ := gin.CreateTestContext(recorder)
  366. (&SUBController{}).ApplyCommonHeaders(ctx, "", "12", "", "", "", "", true, source, false)
  367. if recorder.Header().Get("Routing-Enable") != "true" || !strings.HasPrefix(recorder.Header().Get("Routing"), "happ://routing/onadd/") {
  368. t.Fatalf("headers = %#v", recorder.Header())
  369. }
  370. recorder = httptest.NewRecorder()
  371. ctx, _ = gin.CreateTestContext(recorder)
  372. (&SUBController{}).ApplyCommonHeaders(ctx, "", "12", "", "", "", "", false, source, false)
  373. if recorder.Header().Get("Routing-Enable") != "" || !strings.HasPrefix(recorder.Header().Get("Routing"), "happ://routing/onadd/") {
  374. t.Fatalf("independent routing headers = %#v", recorder.Header())
  375. }
  376. routingSourceResolver = newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  377. return remoteRoutingResponse(http.StatusOK, "routing.help"), nil
  378. }), false)
  379. recorder = httptest.NewRecorder()
  380. ctx, _ = gin.CreateTestContext(recorder)
  381. (&SUBController{}).ApplyCommonHeaders(ctx, "", "12", "", "", "", "", true, "https://example.com/bad", false)
  382. if recorder.Header().Get("Routing-Enable") != "true" || recorder.Header().Get("Routing") != "" {
  383. t.Fatalf("invalid remote source leaked routing headers: %#v", recorder.Header())
  384. }
  385. waitRemoteRoutingIdle(t, routingSourceResolver)
  386. }
  387. func TestResolveIncyRemoteSourceUsesAutorouting(t *testing.T) {
  388. got, remote, err := resolveIncyRoutingSource("https://example.com/DEFAULT.JSON")
  389. if err != nil || !remote || got != "incy://autorouting/onadd/https://example.com/DEFAULT.JSON" {
  390. t.Fatalf("got=%q remote=%v err=%v", got, remote, err)
  391. }
  392. inline := "incy://routing/onadd/abc"
  393. if got, remote, err := resolveIncyRoutingSource(inline); err != nil || remote || got != inline {
  394. t.Fatalf("inline got=%q remote=%v err=%v", got, remote, err)
  395. }
  396. }
  397. func TestMergeRemoteClashRulesPreservesGeneratedProxies(t *testing.T) {
  398. originalProxy := map[string]any{"name": "vpn-node", "type": "vless"}
  399. base := map[string]any{
  400. "proxies": []map[string]any{originalProxy},
  401. "proxy-groups": []map[string]any{{
  402. "name": "PROXY", "type": "select", "proxies": []string{"vpn-node", "DIRECT"},
  403. }},
  404. "rules": []string{"MATCH,PROXY"},
  405. }
  406. remote := `
  407. proxies:
  408. - name: attacker-controlled
  409. proxy-providers:
  410. prov:
  411. url: <SUBSCRIPTION PLACEHOLDER>
  412. external-controller: 0.0.0.0:9090
  413. allow-lan: true
  414. mixed-port: 7890
  415. dns:
  416. enable: true
  417. tun:
  418. enable: true
  419. proxy-groups:
  420. - name: VPN
  421. type: select
  422. include-all: true
  423. - name: PROXY
  424. type: select
  425. proxies: [VPN]
  426. rule-providers:
  427. roscom:
  428. type: http
  429. url: https://example.com/rules.mrs
  430. rules:
  431. - RULE-SET,roscom,PROXY
  432. - MATCH,PROXY
  433. `
  434. if err := mergeRemoteClashRulesYAML(base, remote); err != nil {
  435. t.Fatalf("merge: %v", err)
  436. }
  437. proxies, ok := base["proxies"].([]map[string]any)
  438. if !ok || len(proxies) != 1 || proxies[0]["name"] != "vpn-node" {
  439. t.Fatalf("generated proxies were replaced: %#v", base["proxies"])
  440. }
  441. if _, exists := base["proxy-providers"]; exists {
  442. t.Fatal("remote proxy-providers were imported")
  443. }
  444. if _, exists := base["external-controller"]; exists {
  445. t.Fatal("unsafe top-level key was imported")
  446. }
  447. for _, key := range []string{"allow-lan", "mixed-port", "dns", "tun"} {
  448. if _, exists := base[key]; exists {
  449. t.Fatalf("client-local key %q was imported", key)
  450. }
  451. }
  452. if _, exists := base["rule-providers"]; !exists {
  453. t.Fatal("rule-providers were not imported")
  454. }
  455. groups, ok := asAnySlice(base["proxy-groups"])
  456. if !ok || len(groups) != 2 || clashProxyGroupName(groups[0]) != "VPN" || clashProxyGroupName(groups[1]) != "PROXY" {
  457. t.Fatalf("proxy groups = %#v", base["proxy-groups"])
  458. }
  459. rules, ok := asAnySlice(base["rules"])
  460. if !ok || len(rules) != 2 || rules[1] != "MATCH,PROXY" {
  461. t.Fatalf("rules = %#v", base["rules"])
  462. }
  463. }
  464. func TestMergeRemoteClashRulesKeepsBaseProxyGroupWhenRemoteOmitsIt(t *testing.T) {
  465. base := map[string]any{
  466. "proxies": []map[string]any{{"name": "vpn-node", "type": "vless"}},
  467. "proxy-groups": []map[string]any{{
  468. "name": "PROXY", "type": "select", "proxies": []string{"vpn-node", "DIRECT"},
  469. }},
  470. "rules": []string{"MATCH,PROXY"},
  471. }
  472. if err := mergeRemoteClashRulesYAML(base, `proxy-groups:
  473. - name: Extra
  474. type: select
  475. proxies: [PROXY]
  476. rules:
  477. - MATCH,PROXY
  478. `); err != nil {
  479. t.Fatalf("merge: %v", err)
  480. }
  481. groups, ok := asAnySlice(base["proxy-groups"])
  482. if !ok || len(groups) != 2 || clashProxyGroupName(groups[0]) != "Extra" || clashProxyGroupName(groups[1]) != "PROXY" {
  483. t.Fatalf("proxy groups = %#v", base["proxy-groups"])
  484. }
  485. }
  486. func TestRemoteRoutingRejectsOversizedHappValues(t *testing.T) {
  487. largeJSON := `{"Name":"large","Rules":"` + strings.Repeat("a", remoteRoutingHappMaxValue) + `"}`
  488. largeDeeplink, err := normalizeHappRouting([]byte(largeJSON))
  489. if err != nil {
  490. t.Fatalf("prepare large deeplink: %v", err)
  491. }
  492. tests := []struct {
  493. name string
  494. response func(*http.Request) *http.Response
  495. wantErr string
  496. }{
  497. {
  498. name: "response body",
  499. response: func(*http.Request) *http.Response {
  500. return remoteRoutingResponse(http.StatusOK, strings.Repeat("x", remoteRoutingHappMaxBody+1))
  501. },
  502. wantErr: "response exceeds the size limit",
  503. },
  504. {
  505. name: "normalized header",
  506. response: func(*http.Request) *http.Response {
  507. return remoteRoutingResponse(http.StatusOK, largeJSON)
  508. },
  509. wantErr: "header exceeds the size limit",
  510. },
  511. {
  512. name: "redirect header",
  513. response: func(req *http.Request) *http.Response {
  514. response := remoteRoutingResponse(http.StatusFound, "")
  515. response.Header.Set("Location", largeDeeplink)
  516. response.Request = req
  517. return response
  518. },
  519. wantErr: "header exceeds the size limit",
  520. },
  521. }
  522. for _, tt := range tests {
  523. t.Run(tt.name, func(t *testing.T) {
  524. client := remoteRoutingTestClient(func(req *http.Request) (*http.Response, error) {
  525. return tt.response(req), nil
  526. })
  527. client.CheckRedirect = checkRemoteRoutingRedirect
  528. resolver := newRemoteRoutingResolver(client, false)
  529. err := resolver.refreshSource(remoteRoutingHapp, "https://example.com/rules")
  530. if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
  531. t.Fatalf("err=%v, want %q", err, tt.wantErr)
  532. }
  533. })
  534. }
  535. }
  536. func TestRemoteRoutingRefreshTurnsPanicsIntoErrors(t *testing.T) {
  537. client := remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  538. panic("transport exploded")
  539. })
  540. resolver := newRemoteRoutingResolver(client, false)
  541. err := resolver.refreshSource(remoteRoutingHapp, "https://example.com/rules")
  542. if err == nil || !strings.Contains(err.Error(), "panicked") {
  543. t.Fatalf("err=%v, want the panic converted into an error", err)
  544. }
  545. // The inflight slot must be released so later refreshes are not wedged.
  546. waitRemoteRoutingIdle(t, resolver)
  547. }
  548. func TestRemoteRoutingHTTPClientRejectsLoopback(t *testing.T) {
  549. resolver := newRemoteRoutingResolver(newRemoteRoutingHTTPClient(), false)
  550. startedAt := time.Now()
  551. err := resolver.refreshSource(remoteRoutingHapp, "https://127.0.0.1:1/rules")
  552. if err == nil {
  553. t.Fatal("loopback remote source was accepted")
  554. }
  555. if elapsed := time.Since(startedAt); elapsed > 2*time.Second {
  556. t.Fatalf("loopback rejection took %v", elapsed)
  557. }
  558. }
  559. func TestRemoteRoutingPersistedLoadRetriesAfterDatabaseBecomesReady(t *testing.T) {
  560. dbPath := filepath.Join(t.TempDir(), "x-ui.db")
  561. if err := database.InitDB(dbPath); err != nil {
  562. t.Fatalf("init db: %v", err)
  563. }
  564. t.Cleanup(func() { _ = database.CloseDB() })
  565. deeplink, err := normalizeHappRouting([]byte(`{"Name":"persisted-after-ready"}`))
  566. if err != nil {
  567. t.Fatalf("normalize: %v", err)
  568. }
  569. const source = "https://example.com/default.json"
  570. newRemoteRoutingResolver(nil, false).persistEntry(remoteRoutingHapp, remoteRoutingCacheEntry{
  571. Source: source, Content: deeplink, FetchedAt: time.Now().Unix(),
  572. })
  573. if err := database.CloseDB(); err != nil {
  574. t.Fatalf("close db: %v", err)
  575. }
  576. resolver := newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  577. return remoteRoutingResponse(http.StatusServiceUnavailable, "offline"), nil
  578. }), true)
  579. if _, _, err := resolver.resolve(remoteRoutingHapp, source); !errors.Is(err, errRemoteRoutingUnavailable) {
  580. t.Fatalf("closed-db resolve err=%v", err)
  581. }
  582. waitRemoteRoutingIdle(t, resolver)
  583. waitRemoteRoutingLoadIdle(t, resolver)
  584. if err := database.InitDB(dbPath); err != nil {
  585. t.Fatalf("reopen db: %v", err)
  586. }
  587. resolver.triggerPersistedLoad()
  588. waitRemoteRoutingLoadIdle(t, resolver)
  589. got, remote, err := resolver.resolve(remoteRoutingHapp, source)
  590. if err != nil || !remote || got != deeplink {
  591. t.Fatalf("reloaded resolve got=%q remote=%v err=%v", got, remote, err)
  592. }
  593. }
  594. func TestRemoteClashRouteGraphValidation(t *testing.T) {
  595. tests := []struct {
  596. name string
  597. remote string
  598. wantErr string
  599. }{
  600. {
  601. name: "missing group name",
  602. remote: "proxy-groups:\n - type: select\n proxies: [vpn-node]\nrules:\n - MATCH,PROXY\n",
  603. wantErr: "named group maps",
  604. },
  605. {
  606. name: "duplicate group name",
  607. remote: "proxy-groups:\n - {name: A, type: select, proxies: [vpn-node]}\n - {name: A, type: select, proxies: [vpn-node]}\nrules:\n - MATCH,A\n",
  608. wantErr: "duplicated",
  609. },
  610. {
  611. name: "unknown group reference",
  612. remote: "proxy-groups:\n - {name: A, type: select, proxies: [missing]}\nrules:\n - MATCH,A\n",
  613. wantErr: "unknown proxy or group",
  614. },
  615. {
  616. name: "remote proxy provider use",
  617. remote: "proxy-groups:\n - name: A\n type: select\n use: [manual-provider]\nrules:\n - MATCH,A\n",
  618. wantErr: "cannot use proxy-providers",
  619. },
  620. {
  621. name: "unknown rule provider",
  622. remote: "proxy-groups:\n - {name: A, type: select, proxies: [vpn-node]}\nrules:\n - RULE-SET,missing,A\n - MATCH,A\n",
  623. wantErr: "unknown rule-provider",
  624. },
  625. {
  626. name: "unknown rule target",
  627. remote: "proxy-groups:\n - {name: A, type: select, proxies: [vpn-node]}\nrules:\n - MATCH,missing\n",
  628. wantErr: "unknown proxy or group",
  629. },
  630. {
  631. name: "unknown provider download proxy",
  632. remote: "proxy-groups:\n - {name: A, type: select, proxies: [vpn-node]}\nrule-providers:\n p: {type: http, url: https://example.com/p.mrs, proxy: missing}\nrules:\n - RULE-SET,p,A\n - MATCH,A\n",
  633. wantErr: "rule-provider \"p\" references unknown",
  634. },
  635. }
  636. for _, tt := range tests {
  637. t.Run(tt.name, func(t *testing.T) {
  638. base := map[string]any{
  639. "proxies": []map[string]any{{"name": "vpn-node", "type": "vless"}},
  640. "proxy-groups": []map[string]any{{
  641. "name": "PROXY", "type": "select", "proxies": []string{"vpn-node", "DIRECT"},
  642. }},
  643. "rules": []string{"MATCH,PROXY"},
  644. }
  645. err := mergeRemoteClashRulesYAML(base, tt.remote)
  646. if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
  647. t.Fatalf("err=%v, want %q", err, tt.wantErr)
  648. }
  649. })
  650. }
  651. }
  652. func TestRemoteClashRouteGraphAcceptsLogicalRulesAndCachedDocument(t *testing.T) {
  653. const remote = `
  654. proxy-groups:
  655. - name: Auto
  656. type: url-test
  657. include-all: true
  658. - name: Video
  659. type: select
  660. proxies: [Auto, DIRECT]
  661. rule-providers:
  662. video:
  663. type: http
  664. url: https://example.com/video.mrs
  665. proxy: Auto
  666. rules:
  667. - RULE-SET,video,Video
  668. - AND,((NETWORK,TCP),(DST-PORT,443)),Video
  669. - GEOIP,private,DIRECT,no-resolve
  670. - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve,src
  671. - MATCH,Auto
  672. `
  673. var requests atomic.Int32
  674. resolver := newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
  675. requests.Add(1)
  676. return remoteRoutingResponse(http.StatusOK, remote), nil
  677. }), false)
  678. const source = "https://example.com/routing.yaml"
  679. if err := resolver.refreshSource(remoteRoutingClash, source); err != nil {
  680. t.Fatalf("refresh: %v", err)
  681. }
  682. entry, remoteSource, err := resolver.resolveEntry(remoteRoutingClash, source)
  683. if err != nil || !remoteSource || entry.Clash == nil {
  684. t.Fatalf("entry remote=%v parsed=%v err=%v", remoteSource, entry.Clash != nil, err)
  685. }
  686. base := map[string]any{
  687. "proxies": []map[string]any{{"name": "vpn-node", "type": "vless"}},
  688. "proxy-groups": []map[string]any{{
  689. "name": "PROXY", "type": "select", "proxies": []string{"vpn-node", "DIRECT"},
  690. }},
  691. "rules": []string{"MATCH,PROXY"},
  692. }
  693. if err := mergeRemoteClashRules(base, entry.Clash); err != nil {
  694. t.Fatalf("merge cached document: %v", err)
  695. }
  696. if requests.Load() != 1 {
  697. t.Fatalf("requests=%d, want 1", requests.Load())
  698. }
  699. }