remote_routing_test.go 25 KB

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