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