xray_setting.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package service
  2. import (
  3. _ "embed"
  4. "encoding/json"
  5. "github.com/mhsanaei/3x-ui/v2/util/common"
  6. "github.com/mhsanaei/3x-ui/v2/xray"
  7. )
  8. // XraySettingService provides business logic for Xray configuration management.
  9. // It handles validation and storage of Xray template configurations.
  10. type XraySettingService struct {
  11. SettingService
  12. }
  13. func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
  14. // The frontend round-trips the whole getXraySetting response back
  15. // through the textarea, so if it has ever received a wrapped
  16. // payload (see UnwrapXrayTemplateConfig) it sends that same wrapper
  17. // back here. Strip it before validation/storage, otherwise we save
  18. // garbage the next read can't recover from without this same call.
  19. newXraySettings = UnwrapXrayTemplateConfig(newXraySettings)
  20. if err := s.CheckXrayConfig(newXraySettings); err != nil {
  21. return err
  22. }
  23. if hoisted, err := EnsureStatsRouting(newXraySettings); err == nil {
  24. newXraySettings = hoisted
  25. }
  26. return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings)
  27. }
  28. func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
  29. xrayConfig := &xray.Config{}
  30. err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig)
  31. if err != nil {
  32. return common.NewError("xray template config invalid:", err)
  33. }
  34. return nil
  35. }
  36. // UnwrapXrayTemplateConfig returns the raw xray config JSON from `raw`,
  37. // peeling off any number of `{ "inboundTags": ..., "outboundTestUrl": ...,
  38. // "xraySetting": <real config> }` response-shaped wrappers that may have
  39. // ended up in the database.
  40. //
  41. // How it got there: getXraySetting used to embed the raw DB value as
  42. // `xraySetting` in its response without checking whether the stored
  43. // value was already that exact response shape. If the frontend then
  44. // saved it verbatim (the textarea is a round-trip of the JSON it was
  45. // handed), the wrapper got persisted — and each subsequent save nested
  46. // another layer, producing the blank Xray Settings page reported in
  47. // issue #4059.
  48. //
  49. // If `raw` does not look like a wrapper, it is returned unchanged.
  50. func UnwrapXrayTemplateConfig(raw string) string {
  51. const maxDepth = 8 // defensive cap against pathological multi-nest values
  52. for i := 0; i < maxDepth; i++ {
  53. var top map[string]json.RawMessage
  54. if err := json.Unmarshal([]byte(raw), &top); err != nil {
  55. return raw
  56. }
  57. inner, ok := top["xraySetting"]
  58. if !ok {
  59. return raw
  60. }
  61. // Real xray configs never contain a top-level "xraySetting" key,
  62. // but they do contain things like "inbounds"/"outbounds"/"api".
  63. // If any of those are present, we're already at the real config
  64. // and the "xraySetting" field is either user data or coincidence
  65. // — don't touch it.
  66. for _, k := range []string{"inbounds", "outbounds", "routing", "api", "dns", "log", "policy", "stats"} {
  67. if _, hit := top[k]; hit {
  68. return raw
  69. }
  70. }
  71. // Peel off one layer.
  72. unwrapped := string(inner)
  73. // `xraySetting` may be stored either as a JSON object or as a
  74. // JSON-encoded string of an object. Handle both.
  75. var asStr string
  76. if err := json.Unmarshal(inner, &asStr); err == nil {
  77. unwrapped = asStr
  78. }
  79. raw = unwrapped
  80. }
  81. return raw
  82. }
  83. // EnsureStatsRouting hoists the `api -> api` routing rule to the front
  84. // of routing.rules so the stats query path is never starved by a
  85. // catch-all rule the admin may have added or reordered above it.
  86. //
  87. // Why this matters (#4113, #2818): an admin who adds a cascade outbound
  88. // (e.g. vless to another server) and a routing rule sending all inbound
  89. // traffic to it ends up sending the internal stats inbound's traffic to
  90. // that cascade too, since rules are evaluated top-to-bottom and the
  91. // catch-all matches first. The panel's gRPC stats query then can't reach
  92. // the running xray instance, GetTraffic returns nothing, and every
  93. // client appears offline with zero traffic even though the actual proxy
  94. // path works fine.
  95. //
  96. // The api inbound is special-cased internal infrastructure for the
  97. // panel, not something the admin should ever route to a real outbound.
  98. // Keeping its rule pinned at index 0 is the only correct configuration.
  99. //
  100. // If the api rule is already at index 0 the input is returned unchanged.
  101. // If it exists somewhere else it is moved. If it is missing entirely a
  102. // default rule (`type=field, inboundTag=[api], outboundTag=api`) is
  103. // inserted at the front. Other routing entries keep their relative order.
  104. func EnsureStatsRouting(raw string) (string, error) {
  105. var cfg map[string]json.RawMessage
  106. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  107. return raw, err
  108. }
  109. var routing map[string]json.RawMessage
  110. if r, ok := cfg["routing"]; ok && len(r) > 0 {
  111. if err := json.Unmarshal(r, &routing); err != nil {
  112. return raw, err
  113. }
  114. }
  115. if routing == nil {
  116. routing = make(map[string]json.RawMessage)
  117. }
  118. var rules []map[string]any
  119. if r, ok := routing["rules"]; ok && len(r) > 0 {
  120. if err := json.Unmarshal(r, &rules); err != nil {
  121. return raw, err
  122. }
  123. }
  124. apiIdx := findApiRule(rules)
  125. if apiIdx == 0 {
  126. return raw, nil // already correct, don't churn the JSON
  127. }
  128. var apiRule map[string]any
  129. if apiIdx > 0 {
  130. apiRule = rules[apiIdx]
  131. rules = append(rules[:apiIdx], rules[apiIdx+1:]...)
  132. } else {
  133. apiRule = map[string]any{
  134. "type": "field",
  135. "inboundTag": []string{"api"},
  136. "outboundTag": "api",
  137. }
  138. }
  139. rules = append([]map[string]any{apiRule}, rules...)
  140. rulesJSON, err := json.Marshal(rules)
  141. if err != nil {
  142. return raw, err
  143. }
  144. routing["rules"] = rulesJSON
  145. routingJSON, err := json.Marshal(routing)
  146. if err != nil {
  147. return raw, err
  148. }
  149. cfg["routing"] = routingJSON
  150. out, err := json.Marshal(cfg)
  151. if err != nil {
  152. return raw, err
  153. }
  154. return string(out), nil
  155. }
  156. // findApiRule returns the index of the routing rule that targets the
  157. // internal api inbound (inboundTag contains "api" and outboundTag is
  158. // "api"), or -1 if no such rule exists.
  159. func findApiRule(rules []map[string]any) int {
  160. for i, rule := range rules {
  161. if outTag, _ := rule["outboundTag"].(string); outTag != "api" {
  162. continue
  163. }
  164. raw, ok := rule["inboundTag"]
  165. if !ok {
  166. continue
  167. }
  168. // inboundTag is usually []string but can come as []any from a
  169. // roundtrip through map[string]any. Accept both shapes.
  170. switch tags := raw.(type) {
  171. case []any:
  172. for _, t := range tags {
  173. if s, ok := t.(string); ok && s == "api" {
  174. return i
  175. }
  176. }
  177. case []string:
  178. for _, s := range tags {
  179. if s == "api" {
  180. return i
  181. }
  182. }
  183. case string:
  184. if tags == "api" {
  185. return i
  186. }
  187. }
  188. }
  189. return -1
  190. }