xray_setting_dns_routing.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. package service
  2. import (
  3. "encoding/json"
  4. "net"
  5. "reflect"
  6. "slices"
  7. "sort"
  8. "strconv"
  9. "strings"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
  11. )
  12. // dnsAllowRuleTag marks the routing rules this file manages. Both
  13. // xray-core and the Routing tab's rule editor round-trip ruleTag
  14. // untouched, so it is a stable provenance marker: only rules carrying it
  15. // (or legacy managed rules recognized by exact content, see
  16. // managedDnsAllowRule) are ever stripped and rebuilt.
  17. const dnsAllowRuleTag = "xui-dns-allow"
  18. // managedDnsAllowRule reports whether a routing rule was created by this
  19. // file. Current managed rules carry dnsAllowRuleTag. Rules written before
  20. // the tag existed are adopted only when their shape matches AND their
  21. // exact ip-set/port content equals a currently configured private DNS
  22. // endpoint group — a hand-written rule that merely resembles the managed
  23. // shape (e.g. a CIDR allow for a NAS on port 5000, #6056) never matches
  24. // and is left untouched.
  25. func managedDnsAllowRule(rule map[string]any, groups []dnsAllowPortGroup) bool {
  26. if tag, _ := rule["ruleTag"].(string); tag == dnsAllowRuleTag {
  27. return true
  28. }
  29. if !dnsAllowRuleShape(rule) {
  30. return false
  31. }
  32. port, _ := rule["port"].(string)
  33. ips := append([]string(nil), readRuleIPs(rule["ip"])...)
  34. sort.Strings(ips)
  35. for _, g := range groups {
  36. if strconv.Itoa(g.port) == port && slices.Equal(ips, g.ips) {
  37. return true
  38. }
  39. }
  40. return false
  41. }
  42. // dnsAllowRuleShape identifies the legacy managed-rule shape: a plain
  43. // "type=field, ip=[...], port=..., outboundTag=direct" rule with no other
  44. // matchers. An "enabled" key is tolerated as long as it's true — the
  45. // Routing tab's rule editor (RuleFormModal.tsx submit()) and its enabled
  46. // switch (RoutingTab.tsx toggleRule()) always write that key back, even
  47. // when nothing else changed, so requiring its absence would disown the
  48. // rule the first time an admin so much as opens it in the UI. A rule
  49. // toggled off (enabled=false) is treated as no longer ours: the admin
  50. // explicitly turned it off, and re-enabling it on the next save would
  51. // silently override that choice.
  52. func dnsAllowRuleShape(rule map[string]any) bool {
  53. if t, _ := rule["type"].(string); t != "field" {
  54. return false
  55. }
  56. if out, _ := rule["outboundTag"].(string); out != "direct" {
  57. return false
  58. }
  59. if _, ok := rule["ip"]; !ok {
  60. return false
  61. }
  62. if _, ok := rule["port"]; !ok {
  63. return false
  64. }
  65. for key := range rule {
  66. switch key {
  67. case "type", "outboundTag", "ip", "port":
  68. continue
  69. case "enabled":
  70. if enabled, ok := rule[key].(bool); !ok || !enabled {
  71. return false
  72. }
  73. continue
  74. default:
  75. return false
  76. }
  77. }
  78. return true
  79. }
  80. // findPrivateBlockRule returns the index of a routing rule that blocks
  81. // geoip:private (the panel's default anti-SSRF rule), or -1 if none is
  82. // present. Matched by shape (outboundTag=blocked, ip contains
  83. // "geoip:private") rather than position, since admins can reorder rules.
  84. func findPrivateBlockRule(rules []map[string]any) int {
  85. for i, rule := range rules {
  86. if out, _ := rule["outboundTag"].(string); out != "blocked" {
  87. continue
  88. }
  89. for _, ip := range readRuleIPs(rule["ip"]) {
  90. if strings.EqualFold(ip, "geoip:private") {
  91. return i
  92. }
  93. }
  94. }
  95. return -1
  96. }
  97. func readRuleIPs(raw any) []string {
  98. switch v := raw.(type) {
  99. case []string:
  100. return v
  101. case []any:
  102. out := make([]string, 0, len(v))
  103. for _, item := range v {
  104. if s, ok := item.(string); ok {
  105. out = append(out, s)
  106. }
  107. }
  108. return out
  109. case string:
  110. if v == "" {
  111. return nil
  112. }
  113. return []string{v}
  114. default:
  115. return nil
  116. }
  117. }
  118. // dnsServerEndpoint is a literal (ip, port) pair extracted from a
  119. // dns.servers entry.
  120. type dnsServerEndpoint struct {
  121. ip string
  122. port int
  123. }
  124. // privateDnsServerEndpoint extracts a literal, private/internal (ip, port)
  125. // endpoint from a dns.servers entry, or ok=false if the entry is a domain
  126. // name, a special Xray keyword (localhost, fakedns, ...), or resolves to a
  127. // public IP.
  128. //
  129. // A dns.servers entry is either a bare string or an object with an
  130. // "address" field (see frontend/src/schemas/dns.ts DnsServerEntrySchema);
  131. // the object form may also carry an explicit "port" (default 53 there,
  132. // per DnsServerObjectInnerSchema), which takes precedence over any port
  133. // embedded in the address itself.
  134. func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
  135. var address string
  136. explicitPort := 0
  137. switch v := entry.(type) {
  138. case string:
  139. address = v
  140. case map[string]any:
  141. address, _ = v["address"].(string)
  142. if p, ok := v["port"].(float64); ok && p > 0 {
  143. explicitPort = int(p)
  144. }
  145. default:
  146. return dnsServerEndpoint{}, false
  147. }
  148. host, port := splitAddressHostPort(address)
  149. if host == "" {
  150. return dnsServerEndpoint{}, false
  151. }
  152. if explicitPort > 0 {
  153. port = explicitPort
  154. }
  155. ip := net.ParseIP(host)
  156. if ip == nil {
  157. // Domain name, or a special keyword like "localhost"/"fakedns" —
  158. // neither is something we can safely allow-list by IP here.
  159. return dnsServerEndpoint{}, false
  160. }
  161. if !netsafe.IsBlockedIP(ip) {
  162. return dnsServerEndpoint{}, false
  163. }
  164. return dnsServerEndpoint{ip: ip.String(), port: port}, true
  165. }
  166. // splitAddressHostPort extracts the bare host and port (defaulting to 53)
  167. // from an Xray-core DNS server address string. Those may carry a URI
  168. // scheme (tcp://, tcp+local://, https://, https+local://, quic://,
  169. // quic+local://) and, for DoH, a path and/or a bracketed IPv6 host — all
  170. // of that is stripped down to host[:port] before parsing.
  171. func splitAddressHostPort(address string) (host string, port int) {
  172. address = strings.TrimSpace(address)
  173. if address == "" {
  174. return "", 0
  175. }
  176. if idx := strings.Index(address, "://"); idx != -1 {
  177. address = address[idx+3:]
  178. }
  179. // Drop a DoH path, e.g. "1.1.1.1/dns-query".
  180. if idx := strings.Index(address, "/"); idx != -1 {
  181. address = address[:idx]
  182. }
  183. port = 53
  184. host = address
  185. if strings.HasPrefix(host, "[") {
  186. // Bracketed IPv6, with or without a port: "[::1]" / "[::1]:53".
  187. end := strings.Index(host, "]")
  188. if end == -1 {
  189. return host, port
  190. }
  191. rest := host[end+1:]
  192. host = host[1:end]
  193. if p, ok := strings.CutPrefix(rest, ":"); ok {
  194. if n, err := strconv.Atoi(p); err == nil {
  195. port = n
  196. }
  197. }
  198. return host, port
  199. }
  200. if h, p, err := net.SplitHostPort(host); err == nil {
  201. host = h
  202. if n, err := strconv.Atoi(p); err == nil {
  203. port = n
  204. }
  205. }
  206. return host, port
  207. }
  208. // dnsAllowPortGroup is the set of private literal IPs that share a single
  209. // port among the configured dns.servers, e.g. two internal resolvers both
  210. // queried on :53.
  211. type dnsAllowPortGroup struct {
  212. port int
  213. ips []string
  214. }
  215. // collectPrivateDnsAllowGroups returns the private dns.servers endpoints
  216. // grouped by port, sorted by port ascending (ips within a group sorted and
  217. // de-duplicated) for deterministic output.
  218. func collectPrivateDnsAllowGroups(dnsRaw json.RawMessage) []dnsAllowPortGroup {
  219. if len(dnsRaw) == 0 {
  220. return nil
  221. }
  222. var dns struct {
  223. Servers []any `json:"servers"`
  224. }
  225. if err := json.Unmarshal(dnsRaw, &dns); err != nil {
  226. return nil
  227. }
  228. byPort := make(map[int]map[string]bool)
  229. for _, entry := range dns.Servers {
  230. ep, ok := privateDnsServerEndpoint(entry)
  231. if !ok {
  232. continue
  233. }
  234. if byPort[ep.port] == nil {
  235. byPort[ep.port] = make(map[string]bool)
  236. }
  237. byPort[ep.port][ep.ip] = true
  238. }
  239. ports := make([]int, 0, len(byPort))
  240. for p := range byPort {
  241. ports = append(ports, p)
  242. }
  243. sort.Ints(ports)
  244. groups := make([]dnsAllowPortGroup, 0, len(ports))
  245. for _, p := range ports {
  246. ips := make([]string, 0, len(byPort[p]))
  247. for ip := range byPort[p] {
  248. ips = append(ips, ip)
  249. }
  250. sort.Strings(ips)
  251. groups = append(groups, dnsAllowPortGroup{port: p, ips: ips})
  252. }
  253. return groups
  254. }
  255. // EnsureDnsServerRouting keeps a set of managed "direct" allow-rules — one
  256. // per distinct port among any private/internal dns.servers addresses —
  257. // in sync, positioned immediately before the panel's default
  258. // geoip:private block rule.
  259. //
  260. // Why this matters: Xray's own DNS client traffic is dispatched through
  261. // the same routing table as proxied client traffic. If dns.servers points
  262. // at a private IP (e.g. a self-hosted AdGuard Home / Pi-hole reachable on
  263. // the same Docker network as Xray — a common self-hosted setup) and the
  264. // panel's default private-IP block rule is active, Xray's own DNS lookups
  265. // get silently dropped by that rule. Xray then falls back to dialing
  266. // destinations by raw hostname once its internal DNS attempt times out
  267. // (~4s), so proxied connections still work, just with a multi-second stall
  268. // added to every new domain, with no error surfaced to the client or
  269. // admin.
  270. //
  271. // Each managed rule is scoped to its port (not just the IP), so the
  272. // exception only reopens the DNS traffic that actually needs it rather
  273. // than every port on the private host. On every save, all previously
  274. // managed rules are stripped out and a fresh set is rebuilt from the
  275. // current dns.servers config and reinserted right before the block rule
  276. // (recomputing its index after the strip) — this corrects both content
  277. // drift (dns.servers changed) and position drift (an admin dragged a
  278. // managed rule below the block rule in the Routing tab, which would
  279. // otherwise silently reintroduce the stall with nothing to notice or fix
  280. // it). The rebuilt result is only written back if it actually differs
  281. // from the input, so well-formed configs aren't churned on every save.
  282. // Manually-authored rules are never touched — see managedDnsAllowRule.
  283. func EnsureDnsServerRouting(raw string) (string, error) {
  284. var cfg map[string]json.RawMessage
  285. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  286. return raw, err
  287. }
  288. groups := collectPrivateDnsAllowGroups(cfg["dns"])
  289. var routing map[string]json.RawMessage
  290. if r, ok := cfg["routing"]; ok && len(r) > 0 {
  291. if err := json.Unmarshal(r, &routing); err != nil {
  292. return raw, err
  293. }
  294. }
  295. if routing == nil {
  296. return raw, nil
  297. }
  298. var original []map[string]any
  299. if r, ok := routing["rules"]; ok && len(r) > 0 {
  300. if err := json.Unmarshal(r, &original); err != nil {
  301. return raw, err
  302. }
  303. }
  304. rebuilt := rebuildDnsAllowRules(original, groups)
  305. rulesJSON, err := json.Marshal(rebuilt)
  306. if err != nil {
  307. return raw, err
  308. }
  309. // Compare against the original rules JSON, not the parsed Go values:
  310. // json.Unmarshal into []map[string]any turns "ip" arrays into []any,
  311. // while the rules this function builds use []string — those hold
  312. // identical content but are different types under reflect.DeepEqual,
  313. // which would otherwise report a no-op input as changed and churn the
  314. // JSON on every save for no reason.
  315. origRulesJSON := routing["rules"]
  316. if len(origRulesJSON) == 0 {
  317. origRulesJSON = json.RawMessage("[]")
  318. }
  319. if jsonEqual(origRulesJSON, rulesJSON) {
  320. return raw, nil
  321. }
  322. routing["rules"] = rulesJSON
  323. routingJSON, err := json.Marshal(routing)
  324. if err != nil {
  325. return raw, err
  326. }
  327. cfg["routing"] = routingJSON
  328. out, err := json.Marshal(cfg)
  329. if err != nil {
  330. return raw, err
  331. }
  332. return string(out), nil
  333. }
  334. // rebuildDnsAllowRules strips any existing managed rules out of rules,
  335. // then — if a geoip:private block rule is present and groups is non-empty
  336. // — reinserts a freshly built managed rule per group immediately before
  337. // it. This uniformly handles content updates, position drift, and removal
  338. // (an empty groups list just leaves the managed rules stripped).
  339. func rebuildDnsAllowRules(rules []map[string]any, groups []dnsAllowPortGroup) []map[string]any {
  340. clean := make([]map[string]any, 0, len(rules))
  341. for _, rule := range rules {
  342. if !managedDnsAllowRule(rule, groups) {
  343. clean = append(clean, rule)
  344. }
  345. }
  346. blockIdx := findPrivateBlockRule(clean)
  347. if blockIdx < 0 || len(groups) == 0 {
  348. return clean
  349. }
  350. managed := make([]map[string]any, 0, len(groups))
  351. for _, g := range groups {
  352. managed = append(managed, map[string]any{
  353. "type": "field",
  354. "ruleTag": dnsAllowRuleTag,
  355. "ip": g.ips,
  356. "port": strconv.Itoa(g.port),
  357. "outboundTag": "direct",
  358. })
  359. }
  360. // Capacity hint uses len(clean) alone (not len(clean)+len(managed)):
  361. // summing two independent lengths for a make() size risks overflow on
  362. // pathological input per static analysis, and clean's length already
  363. // covers most of the eventual size on its own.
  364. out := make([]map[string]any, 0, len(clean))
  365. out = append(out, clean[:blockIdx]...)
  366. out = append(out, managed...)
  367. out = append(out, clean[blockIdx:]...)
  368. return out
  369. }
  370. // jsonEqual reports whether a and b decode to structurally identical
  371. // values. Used instead of comparing raw bytes (key order, whitespace) or
  372. // reflect.DeepEqual on already-parsed Go values (which is type-sensitive
  373. // to []any vs []string and would misreport identical content as changed).
  374. func jsonEqual(a, b json.RawMessage) bool {
  375. var av, bv any
  376. if err := json.Unmarshal(a, &av); err != nil {
  377. return false
  378. }
  379. if err := json.Unmarshal(b, &bv); err != nil {
  380. return false
  381. }
  382. return reflect.DeepEqual(av, bv)
  383. }