xray_setting_dns_routing_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. )
  6. func rulesFromRaw(t *testing.T, raw string) []map[string]any {
  7. t.Helper()
  8. var cfg map[string]any
  9. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  10. t.Fatalf("unmarshal: %v", err)
  11. }
  12. return routingRulesFromCfg(cfg)
  13. }
  14. func TestEnsureDnsServerRouting_NoOpWithoutDnsServers(t *testing.T) {
  15. in := `{"routing":{"rules":[{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}}`
  16. out, err := EnsureDnsServerRouting(in)
  17. if err != nil {
  18. t.Fatalf("unexpected err: %v", err)
  19. }
  20. if out != in {
  21. t.Fatalf("expected unchanged input, got: %s", out)
  22. }
  23. }
  24. func TestEnsureDnsServerRouting_NoOpForPublicDnsServer(t *testing.T) {
  25. in := `{
  26. "dns": {"servers": ["1.1.1.1"]},
  27. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  28. }`
  29. out, err := EnsureDnsServerRouting(in)
  30. if err != nil {
  31. t.Fatalf("unexpected err: %v", err)
  32. }
  33. if out != in {
  34. t.Fatalf("expected unchanged input for a public DNS server, got: %s", out)
  35. }
  36. }
  37. func TestEnsureDnsServerRouting_NoOpWithoutPrivateBlockRule(t *testing.T) {
  38. // Private DNS server, but nothing in routing would block it — no rule
  39. // needed.
  40. in := `{
  41. "dns": {"servers": ["172.20.0.53"]},
  42. "routing": {"rules": [{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}
  43. }`
  44. out, err := EnsureDnsServerRouting(in)
  45. if err != nil {
  46. t.Fatalf("unexpected err: %v", err)
  47. }
  48. if out != in {
  49. t.Fatalf("expected unchanged input without a private-block rule, got: %s", out)
  50. }
  51. }
  52. func TestEnsureDnsServerRouting_InsertsAllowRuleBeforeBlock(t *testing.T) {
  53. // Reproduces the reported bug: dns.servers on a private docker IP
  54. // (e.g. a same-network AdGuard Home) plus the panel's default
  55. // geoip:private block rule silently drops Xray's own DNS traffic.
  56. in := `{
  57. "dns": {"servers": ["172.20.0.53"], "queryStrategy": "UseIPv4"},
  58. "routing": {
  59. "rules": [
  60. {"type":"field","inboundTag":["api"],"outboundTag":"api"},
  61. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]},
  62. {"type":"field","outboundTag":"blocked","protocol":["bittorrent"]}
  63. ]
  64. }
  65. }`
  66. out, err := EnsureDnsServerRouting(in)
  67. if err != nil {
  68. t.Fatalf("unexpected err: %v", err)
  69. }
  70. rules := rulesFromRaw(t, out)
  71. if len(rules) != 4 {
  72. t.Fatalf("rules len = %d, want 4: %s", len(rules), out)
  73. }
  74. if tag, _ := rules[1]["outboundTag"].(string); tag != "direct" {
  75. t.Fatalf("expected inserted allow-rule at index 1, got outboundTag %v\nfull: %s", rules[1]["outboundTag"], out)
  76. }
  77. ips := readRuleIPs(rules[1]["ip"])
  78. if len(ips) != 1 || ips[0] != "172.20.0.53" {
  79. t.Fatalf("allow-rule ip = %v, want [172.20.0.53]", ips)
  80. }
  81. if port, _ := rules[1]["port"].(string); port != "53" {
  82. t.Fatalf("allow-rule port = %v, want \"53\" (scoped to DNS traffic only)", rules[1]["port"])
  83. }
  84. if tag, _ := rules[2]["outboundTag"].(string); tag != "blocked" {
  85. t.Fatalf("private-block rule should still follow the allow-rule, got %v", rules[2])
  86. }
  87. }
  88. func TestEnsureDnsServerRouting_HandlesObjectServerEntryWithExplicitPort(t *testing.T) {
  89. in := `{
  90. "dns": {"servers": [{"address": "172.20.0.53", "port": 5353}]},
  91. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  92. }`
  93. out, err := EnsureDnsServerRouting(in)
  94. if err != nil {
  95. t.Fatalf("unexpected err: %v", err)
  96. }
  97. rules := rulesFromRaw(t, out)
  98. ips := readRuleIPs(rules[0]["ip"])
  99. if len(ips) != 1 || ips[0] != "172.20.0.53" {
  100. t.Fatalf("allow-rule ip = %v, want [172.20.0.53]", ips)
  101. }
  102. if port, _ := rules[0]["port"].(string); port != "5353" {
  103. t.Fatalf("allow-rule port = %v, want the object's explicit \"5353\"", rules[0]["port"])
  104. }
  105. }
  106. func TestEnsureDnsServerRouting_GroupsDistinctPortsIntoSeparateRules(t *testing.T) {
  107. // Two internal resolvers on different ports must not be merged into
  108. // one ip+port rule — that would cross-allow ip1:port2 and ip2:port1,
  109. // widening the exception beyond what's actually needed.
  110. in := `{
  111. "dns": {"servers": ["172.20.0.53", "10.0.0.53:5353"]},
  112. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  113. }`
  114. out, err := EnsureDnsServerRouting(in)
  115. if err != nil {
  116. t.Fatalf("unexpected err: %v", err)
  117. }
  118. rules := rulesFromRaw(t, out)
  119. if len(rules) != 3 {
  120. t.Fatalf("rules len = %d, want 3 (one rule per port + the block rule): %s", len(rules), out)
  121. }
  122. if p, _ := rules[0]["port"].(string); p != "53" {
  123. t.Fatalf("rules[0] port = %v, want \"53\" (sorted ascending)", rules[0]["port"])
  124. }
  125. if ips := readRuleIPs(rules[0]["ip"]); len(ips) != 1 || ips[0] != "172.20.0.53" {
  126. t.Fatalf("rules[0] ip = %v, want [172.20.0.53]", ips)
  127. }
  128. if p, _ := rules[1]["port"].(string); p != "5353" {
  129. t.Fatalf("rules[1] port = %v, want \"5353\"", rules[1]["port"])
  130. }
  131. if ips := readRuleIPs(rules[1]["ip"]); len(ips) != 1 || ips[0] != "10.0.0.53" {
  132. t.Fatalf("rules[1] ip = %v, want [10.0.0.53]", ips)
  133. }
  134. }
  135. func TestEnsureDnsServerRouting_StripsSchemePortAndPath(t *testing.T) {
  136. in := `{
  137. "dns": {"servers": [
  138. "tcp://10.0.0.53:5353",
  139. "https+local://192.168.1.1/dns-query",
  140. "[fd00::53]:53"
  141. ]},
  142. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  143. }`
  144. out, err := EnsureDnsServerRouting(in)
  145. if err != nil {
  146. t.Fatalf("unexpected err: %v", err)
  147. }
  148. rules := rulesFromRaw(t, out)
  149. // 192.168.1.1 and fd00::53 share the default port 53; 10.0.0.53:5353
  150. // is its own group.
  151. if len(rules) != 3 {
  152. t.Fatalf("rules len = %d, want 3 (2 port groups + block rule): %s", len(rules), out)
  153. }
  154. port53 := rules[0]
  155. if p, _ := port53["port"].(string); p != "53" {
  156. t.Fatalf("rules[0] port = %v, want \"53\"", port53["port"])
  157. }
  158. want53 := map[string]bool{"192.168.1.1": true, "fd00::53": true}
  159. ips := readRuleIPs(port53["ip"])
  160. if len(ips) != len(want53) {
  161. t.Fatalf("rules[0] ip = %v, want entries matching %v", ips, want53)
  162. }
  163. for _, ip := range ips {
  164. if !want53[ip] {
  165. t.Fatalf("unexpected ip %q in port-53 rule %v", ip, ips)
  166. }
  167. }
  168. port5353 := rules[1]
  169. if p, _ := port5353["port"].(string); p != "5353" {
  170. t.Fatalf("rules[1] port = %v, want \"5353\"", port5353["port"])
  171. }
  172. if ips := readRuleIPs(port5353["ip"]); len(ips) != 1 || ips[0] != "10.0.0.53" {
  173. t.Fatalf("rules[1] ip = %v, want [10.0.0.53]", ips)
  174. }
  175. }
  176. func TestEnsureDnsServerRouting_SkipsSpecialAndDomainAddresses(t *testing.T) {
  177. in := `{
  178. "dns": {"servers": ["localhost", "fakedns", "dns.google", "8.8.8.8"]},
  179. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  180. }`
  181. out, err := EnsureDnsServerRouting(in)
  182. if err != nil {
  183. t.Fatalf("unexpected err: %v", err)
  184. }
  185. if out != in {
  186. t.Fatalf("expected unchanged input (no private literal IPs present), got: %s", out)
  187. }
  188. }
  189. func TestEnsureDnsServerRouting_IdempotentOnSecondSave(t *testing.T) {
  190. in := `{
  191. "dns": {"servers": ["172.20.0.53"]},
  192. "routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
  193. }`
  194. first, err := EnsureDnsServerRouting(in)
  195. if err != nil {
  196. t.Fatalf("unexpected err: %v", err)
  197. }
  198. second, err := EnsureDnsServerRouting(first)
  199. if err != nil {
  200. t.Fatalf("unexpected err: %v", err)
  201. }
  202. if second != first {
  203. t.Fatalf("expected no further change on second pass\nfirst: %s\nsecond: %s", first, second)
  204. }
  205. }
  206. func TestEnsureDnsServerRouting_UpdatesOwnedRuleWhenServersChange(t *testing.T) {
  207. // A legacy (untagged) managed rule matching the current dns.servers is
  208. // adopted: rebuilt once with the ruleTag marker, then stable.
  209. in := `{
  210. "dns": {"servers": ["172.20.0.53"]},
  211. "routing": {
  212. "rules": [
  213. {"type":"field","ip":["172.20.0.53"],"port":"53","outboundTag":"direct"},
  214. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  215. ]
  216. }
  217. }`
  218. out, err := EnsureDnsServerRouting(in)
  219. if err != nil {
  220. t.Fatalf("unexpected err: %v", err)
  221. }
  222. rules := rulesFromRaw(t, out)
  223. if len(rules) != 2 {
  224. t.Fatalf("rules len = %d, want 2 (legacy rule adopted in place): %s", len(rules), out)
  225. }
  226. if tag, _ := rules[0]["ruleTag"].(string); tag != dnsAllowRuleTag {
  227. t.Fatalf("adopted rule should carry %q, got %v", dnsAllowRuleTag, rules[0])
  228. }
  229. stable, err := EnsureDnsServerRouting(out)
  230. if err != nil {
  231. t.Fatalf("unexpected err: %v", err)
  232. }
  233. if stable != out {
  234. t.Fatalf("expected no further change once tagged\nfirst: %s\nsecond: %s", out, stable)
  235. }
  236. // Admin adds a second internal resolver on the same port.
  237. in2 := `{
  238. "dns": {"servers": ["172.20.0.53", "10.0.0.53"]},
  239. "routing": {
  240. "rules": [
  241. {"type":"field","ruleTag":"xui-dns-allow","ip":["172.20.0.53"],"port":"53","outboundTag":"direct"},
  242. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  243. ]
  244. }
  245. }`
  246. out2, err := EnsureDnsServerRouting(in2)
  247. if err != nil {
  248. t.Fatalf("unexpected err: %v", err)
  249. }
  250. rules = rulesFromRaw(t, out2)
  251. if len(rules) != 2 {
  252. t.Fatalf("rules len = %d, want 2 (existing rule updated in place): %s", len(rules), out2)
  253. }
  254. ips := readRuleIPs(rules[0]["ip"])
  255. if len(ips) != 2 || ips[0] != "10.0.0.53" || ips[1] != "172.20.0.53" {
  256. t.Fatalf("allow-rule ip = %v, want [10.0.0.53 172.20.0.53]", ips)
  257. }
  258. }
  259. func TestEnsureDnsServerRouting_RemovesOwnedRuleWhenNoLongerNeeded(t *testing.T) {
  260. // Admin switches dns.servers to a public resolver — the tagged managed
  261. // rule is now dead weight and should be dropped.
  262. in := `{
  263. "dns": {"servers": ["1.1.1.1"]},
  264. "routing": {
  265. "rules": [
  266. {"type":"field","ruleTag":"xui-dns-allow","ip":["172.20.0.53"],"port":"53","outboundTag":"direct"},
  267. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  268. ]
  269. }
  270. }`
  271. out, err := EnsureDnsServerRouting(in)
  272. if err != nil {
  273. t.Fatalf("unexpected err: %v", err)
  274. }
  275. rules := rulesFromRaw(t, out)
  276. if len(rules) != 1 {
  277. t.Fatalf("rules len = %d, want 1 (stale allow-rule removed): %s", len(rules), out)
  278. }
  279. if tag, _ := rules[0]["outboundTag"].(string); tag != "blocked" {
  280. t.Fatalf("remaining rule should be the block rule, got %v", rules[0])
  281. }
  282. }
  283. func TestEnsureDnsServerRouting_KeepsManualDirectRuleOnSave(t *testing.T) {
  284. // The #6056 regression: a hand-written "LAN service over direct" rule
  285. // (CIDR ip + custom port, no ruleTag) matches nothing the panel
  286. // manages and must survive a save untouched, even when dns.servers has
  287. // no private entries at all.
  288. in := `{
  289. "dns": {"servers": ["1.1.1.1"]},
  290. "routing": {
  291. "rules": [
  292. {"type":"field","ip":["192.168.178.0/24"],"port":"5000","outboundTag":"direct","enabled":true},
  293. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  294. ]
  295. }
  296. }`
  297. out, err := EnsureDnsServerRouting(in)
  298. if err != nil {
  299. t.Fatalf("unexpected err: %v", err)
  300. }
  301. if out != in {
  302. t.Fatalf("manual direct rule must survive the save untouched, got: %s", out)
  303. }
  304. // Same shape with a single literal private IP — indistinguishable from
  305. // a legacy managed rule only if it matches a configured dns server,
  306. // which it doesn't here, so it must survive too.
  307. in2 := `{
  308. "dns": {"servers": ["1.1.1.1"]},
  309. "routing": {
  310. "rules": [
  311. {"type":"field","ip":["192.168.178.4"],"port":"5000","outboundTag":"direct"},
  312. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  313. ]
  314. }
  315. }`
  316. out2, err := EnsureDnsServerRouting(in2)
  317. if err != nil {
  318. t.Fatalf("unexpected err: %v", err)
  319. }
  320. if out2 != in2 {
  321. t.Fatalf("manual single-IP direct rule must survive the save untouched, got: %s", out2)
  322. }
  323. }
  324. func TestEnsureDnsServerRouting_DoesNotTouchManualRuleWithExtraMatchers(t *testing.T) {
  325. // A hand-written rule that also allows the DNS IP but carries an extra
  326. // matcher isn't recognized as "ours" and must be left alone; a fresh
  327. // managed rule is inserted alongside it instead.
  328. in := `{
  329. "dns": {"servers": ["172.20.0.53"]},
  330. "routing": {
  331. "rules": [
  332. {"type":"field","ip":["172.20.0.53"],"port":"53","domain":["example.com"],"outboundTag":"direct"},
  333. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  334. ]
  335. }
  336. }`
  337. out, err := EnsureDnsServerRouting(in)
  338. if err != nil {
  339. t.Fatalf("unexpected err: %v", err)
  340. }
  341. rules := rulesFromRaw(t, out)
  342. if len(rules) != 3 {
  343. t.Fatalf("rules len = %d, want 3 (manual rule kept, managed rule inserted): %s", len(rules), out)
  344. }
  345. if _, ok := rules[0]["domain"]; !ok {
  346. t.Fatalf("manual rule with domain matcher should be untouched, got %v", rules[0])
  347. }
  348. }
  349. func TestEnsureDnsServerRouting_RepositionsRuleDraggedAfterBlockRule(t *testing.T) {
  350. // The Routing tab lets admins freely drag rules around
  351. // (RoutingTab.tsx move-up/move-down and drag-and-drop), and a managed
  352. // rule renders as an indistinguishable normal row there. If it ends up
  353. // at or after the block rule — directly, or incidentally while
  354. // reordering something else — the exact bug this file fixes comes
  355. // back silently: the block rule matches first and Xray's own DNS
  356. // traffic is dropped again. Detecting only ip/content drift isn't
  357. // enough; position must be checked too.
  358. in := `{
  359. "dns": {"servers": ["172.20.0.53"]},
  360. "routing": {
  361. "rules": [
  362. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]},
  363. {"type":"field","ip":["172.20.0.53"],"port":"53","outboundTag":"direct"}
  364. ]
  365. }
  366. }`
  367. out, err := EnsureDnsServerRouting(in)
  368. if err != nil {
  369. t.Fatalf("unexpected err: %v", err)
  370. }
  371. rules := rulesFromRaw(t, out)
  372. if len(rules) != 2 {
  373. t.Fatalf("rules len = %d, want 2: %s", len(rules), out)
  374. }
  375. if tag, _ := rules[0]["outboundTag"].(string); tag != "direct" {
  376. t.Fatalf("managed rule should be re-homed to index 0 (before the block rule), got %v\nfull: %s", rules[0], out)
  377. }
  378. if tag, _ := rules[1]["outboundTag"].(string); tag != "blocked" {
  379. t.Fatalf("block rule should now be at index 1, got %v\nfull: %s", rules[1], out)
  380. }
  381. }
  382. func TestEnsureDnsServerRouting_TreatsExplicitlyEnabledRuleAsOwned(t *testing.T) {
  383. // RuleFormModal.tsx's submit() always writes an "enabled" key, even
  384. // when the admin changed nothing and it was already true — merely
  385. // opening the auto-generated rule in the editor must not disown it.
  386. in := `{
  387. "dns": {"servers": ["172.20.0.53"]},
  388. "routing": {
  389. "rules": [
  390. {"type":"field","ip":["172.20.0.53"],"port":"53","outboundTag":"direct","enabled":true},
  391. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  392. ]
  393. }
  394. }`
  395. out, err := EnsureDnsServerRouting(in)
  396. if err != nil {
  397. t.Fatalf("unexpected err: %v", err)
  398. }
  399. rules := rulesFromRaw(t, out)
  400. if len(rules) != 2 {
  401. t.Fatalf("rules len = %d, want 2 (no duplicate inserted): %s", len(rules), out)
  402. }
  403. }
  404. func TestEnsureDnsServerRouting_DisabledRuleIsDisownedAndReplaced(t *testing.T) {
  405. // toggleRule() in RoutingTab.tsx writes enabled=false on a plain
  406. // switch flip. An admin who explicitly disables the managed rule is
  407. // choosing to turn the exception off; re-enabling it on the next save
  408. // would silently override that. The disabled rule is left alone and a
  409. // fresh, enabled one is (re-)created to keep the fix working.
  410. in := `{
  411. "dns": {"servers": ["172.20.0.53"]},
  412. "routing": {
  413. "rules": [
  414. {"type":"field","ip":["172.20.0.53"],"port":"53","outboundTag":"direct","enabled":false},
  415. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  416. ]
  417. }
  418. }`
  419. out, err := EnsureDnsServerRouting(in)
  420. if err != nil {
  421. t.Fatalf("unexpected err: %v", err)
  422. }
  423. rules := rulesFromRaw(t, out)
  424. if len(rules) != 3 {
  425. t.Fatalf("rules len = %d, want 3 (disabled rule kept as-is, fresh managed rule added): %s", len(rules), out)
  426. }
  427. if enabled, _ := rules[0]["enabled"].(bool); enabled {
  428. t.Fatalf("original disabled rule should be untouched, got %v", rules[0])
  429. }
  430. if _, ok := rules[1]["enabled"]; ok {
  431. t.Fatalf("freshly (re-)generated rule shouldn't carry an enabled key, got %v", rules[1])
  432. }
  433. if tag, _ := rules[1]["outboundTag"].(string); tag != "direct" {
  434. t.Fatalf("expected the fresh managed rule at index 1, got %v", rules[1])
  435. }
  436. }
  437. func TestEnsureDnsServerRouting_InvalidJsonReturnsAsIs(t *testing.T) {
  438. in := "definitely not json"
  439. out, err := EnsureDnsServerRouting(in)
  440. if err == nil {
  441. t.Fatalf("expected error for invalid json, got none")
  442. }
  443. if out != in {
  444. t.Fatalf("expected raw passthrough on error, got %q", out)
  445. }
  446. }