hot_diff.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. package xray
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  6. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  7. )
  8. // HotDiff describes the gRPC API operations needed to bring a running Xray
  9. // instance from one generated config to another without restarting the
  10. // process. It only covers the sections Xray can reload at runtime: inbounds,
  11. // outbounds and routing rules/balancers.
  12. type HotDiff struct {
  13. RemovedInboundTags []string
  14. AddedInbounds [][]byte
  15. RemovedOutboundTags []string
  16. AddedOutbounds [][]byte
  17. RoutingConfig []byte // full new routing section; nil when unchanged
  18. }
  19. // Empty reports whether the diff contains no operations.
  20. func (d *HotDiff) Empty() bool {
  21. return len(d.RemovedInboundTags) == 0 &&
  22. len(d.AddedInbounds) == 0 &&
  23. len(d.RemovedOutboundTags) == 0 &&
  24. len(d.AddedOutbounds) == 0 &&
  25. d.RoutingConfig == nil
  26. }
  27. // ComputeHotDiff compares two generated configs and returns the API operations
  28. // that transform a running instance from oldCfg to newCfg. ok is false when
  29. // the change touches anything that has no runtime reload API (log, dns,
  30. // policy, ...) and therefore requires a full process restart.
  31. func ComputeHotDiff(oldCfg, newCfg *Config) (*HotDiff, bool) {
  32. if oldCfg == nil || newCfg == nil {
  33. return nil, false
  34. }
  35. // Sections without a reload API must be semantically identical.
  36. // Comparison is whitespace-insensitive: a template save that merely
  37. // reformats the JSON (frontend textarea, API clients) must not be
  38. // mistaken for a real change that forces a restart.
  39. static := []struct {
  40. name string
  41. old, new json_util.RawMessage
  42. }{
  43. {"log", oldCfg.LogConfig, newCfg.LogConfig},
  44. {"dns", oldCfg.DNSConfig, newCfg.DNSConfig},
  45. {"transport", oldCfg.Transport, newCfg.Transport},
  46. {"policy", oldCfg.Policy, newCfg.Policy},
  47. {"api", oldCfg.API, newCfg.API},
  48. {"stats", oldCfg.Stats, newCfg.Stats},
  49. {"reverse", oldCfg.Reverse, newCfg.Reverse},
  50. {"fakedns", oldCfg.FakeDNS, newCfg.FakeDNS},
  51. {"observatory", oldCfg.Observatory, newCfg.Observatory},
  52. {"burstObservatory", oldCfg.BurstObservatory, newCfg.BurstObservatory},
  53. {"metrics", oldCfg.Metrics, newCfg.Metrics},
  54. {"geodata", oldCfg.Geodata, newCfg.Geodata},
  55. }
  56. for _, section := range static {
  57. if !rawEqualNormalized(section.old, section.new) {
  58. logger.Debug("hot diff: section [", section.name, "] changed and has no reload API")
  59. return nil, false
  60. }
  61. }
  62. diff := &HotDiff{}
  63. if ok := diffInbounds(oldCfg, newCfg, diff); !ok {
  64. logger.Debug("hot diff: inbound change is not API-applicable")
  65. return nil, false
  66. }
  67. if ok := diffOutbounds(oldCfg, newCfg, diff); !ok {
  68. logger.Debug("hot diff: outbound change is not API-applicable (default outbound or tags)")
  69. return nil, false
  70. }
  71. if ok := diffRouting(oldCfg, newCfg, diff); !ok {
  72. logger.Debug("hot diff: routing change is not API-applicable (domainStrategy or section shape)")
  73. return nil, false
  74. }
  75. return diff, true
  76. }
  77. // diffInbounds fills diff with inbound removals/additions (a changed inbound
  78. // becomes remove+add). The api inbound carries the gRPC server the panel is
  79. // talking through, so any change touching it forces a restart.
  80. func diffInbounds(oldCfg, newCfg *Config, diff *HotDiff) bool {
  81. oldByTag, ok := inboundsByTag(oldCfg.InboundConfigs)
  82. if !ok {
  83. return false
  84. }
  85. newByTag, ok := inboundsByTag(newCfg.InboundConfigs)
  86. if !ok {
  87. return false
  88. }
  89. apiTag := apiTagFromConfig(newCfg.API)
  90. for i := range oldCfg.InboundConfigs {
  91. oldIb := &oldCfg.InboundConfigs[i]
  92. newIb, exists := newByTag[oldIb.Tag]
  93. if exists && inboundEqualNormalized(oldIb, newIb) {
  94. continue
  95. }
  96. if oldIb.Tag == apiTag || oldIb.Tag == "api" {
  97. return false
  98. }
  99. diff.RemovedInboundTags = append(diff.RemovedInboundTags, oldIb.Tag)
  100. if exists {
  101. raw, err := json.Marshal(newIb)
  102. if err != nil {
  103. return false
  104. }
  105. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  106. }
  107. }
  108. for i := range newCfg.InboundConfigs {
  109. newIb := &newCfg.InboundConfigs[i]
  110. if _, exists := oldByTag[newIb.Tag]; exists {
  111. continue
  112. }
  113. if newIb.Tag == apiTag || newIb.Tag == "api" {
  114. return false
  115. }
  116. raw, err := json.Marshal(newIb)
  117. if err != nil {
  118. return false
  119. }
  120. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  121. }
  122. return true
  123. }
  124. // diffOutbounds fills diff with outbound removals/additions keyed by tag.
  125. // The first outbound is xray's default handler and the API can only append,
  126. // so any change to its identity or content forces a restart. Reordering of
  127. // the remaining outbounds is ignored — routing addresses them by tag.
  128. func diffOutbounds(oldCfg, newCfg *Config, diff *HotDiff) bool {
  129. oldOut, ok := parseOutbounds(oldCfg.OutboundConfigs)
  130. if !ok {
  131. return false
  132. }
  133. newOut, ok := parseOutbounds(newCfg.OutboundConfigs)
  134. if !ok {
  135. return false
  136. }
  137. if (len(oldOut) == 0) != (len(newOut) == 0) {
  138. return false
  139. }
  140. if len(oldOut) > 0 {
  141. if oldOut[0].tag != newOut[0].tag || !bytes.Equal(oldOut[0].norm, newOut[0].norm) {
  142. return false
  143. }
  144. }
  145. oldByTag := make(map[string]outboundEntry, len(oldOut))
  146. for _, e := range oldOut {
  147. oldByTag[e.tag] = e
  148. }
  149. newByTag := make(map[string]outboundEntry, len(newOut))
  150. for _, e := range newOut {
  151. newByTag[e.tag] = e
  152. }
  153. for _, oldE := range oldOut {
  154. newE, exists := newByTag[oldE.tag]
  155. if exists && bytes.Equal(oldE.norm, newE.norm) {
  156. continue
  157. }
  158. diff.RemovedOutboundTags = append(diff.RemovedOutboundTags, oldE.tag)
  159. if exists {
  160. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  161. }
  162. }
  163. for _, newE := range newOut {
  164. if _, exists := oldByTag[newE.tag]; !exists {
  165. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  166. }
  167. }
  168. return true
  169. }
  170. // diffRouting decides whether the routing change is limited to rules and
  171. // balancers — the only parts RoutingService.AddRule can replace at runtime.
  172. // domainStrategy/domainMatcher and any other key in the section are fixed at
  173. // process start.
  174. func diffRouting(oldCfg, newCfg *Config, diff *HotDiff) bool {
  175. if bytes.Equal(oldCfg.RouterConfig, newCfg.RouterConfig) {
  176. return true
  177. }
  178. // No routing section at start likely means no router feature (and no
  179. // RoutingService) in the running instance — only a restart can add it.
  180. if len(oldCfg.RouterConfig) == 0 || len(newCfg.RouterConfig) == 0 {
  181. return false
  182. }
  183. oldRest, ok := routingWithoutReloadable(oldCfg.RouterConfig)
  184. if !ok {
  185. return false
  186. }
  187. newRest, ok := routingWithoutReloadable(newCfg.RouterConfig)
  188. if !ok {
  189. return false
  190. }
  191. if !bytes.Equal(oldRest, newRest) {
  192. return false
  193. }
  194. diff.RoutingConfig = newCfg.RouterConfig
  195. return true
  196. }
  197. // routingWithoutReloadable returns the routing section normalized with the
  198. // runtime-reloadable keys removed, for comparing the restart-only remainder.
  199. func routingWithoutReloadable(raw []byte) ([]byte, bool) {
  200. parsed := map[string]any{}
  201. if len(raw) > 0 {
  202. decoder := json.NewDecoder(bytes.NewReader(raw))
  203. decoder.UseNumber()
  204. if err := decoder.Decode(&parsed); err != nil {
  205. return nil, false
  206. }
  207. }
  208. delete(parsed, "rules")
  209. delete(parsed, "balancers")
  210. out, err := json.Marshal(parsed)
  211. if err != nil {
  212. return nil, false
  213. }
  214. return out, true
  215. }
  216. // inboundEqualNormalized compares two inbounds ignoring JSON formatting in
  217. // their raw sections, so a reformatted template does not read as a changed
  218. // inbound.
  219. func inboundEqualNormalized(a, b *InboundConfig) bool {
  220. return a.Port == b.Port &&
  221. a.Protocol == b.Protocol &&
  222. a.Tag == b.Tag &&
  223. rawEqualNormalized(a.Listen, b.Listen) &&
  224. rawEqualNormalized(a.Settings, b.Settings) &&
  225. rawEqualNormalized(a.StreamSettings, b.StreamSettings) &&
  226. rawEqualNormalized(a.Sniffing, b.Sniffing)
  227. }
  228. // rawEqualNormalized reports whether two raw JSON values are semantically
  229. // equal: whitespace, object key order and an explicit `null` versus an
  230. // absent section are all ignored. UI editors rebuild objects on save (new
  231. // key order) and emit `null` for switched-off sections — none of that is a
  232. // reason to restart the core. Number precision is preserved via json.Number,
  233. // so genuinely different values never compare equal. Unparsable values only
  234. // compare equal byte-for-byte.
  235. func rawEqualNormalized(a, b json_util.RawMessage) bool {
  236. if bytes.Equal(a, b) {
  237. return true
  238. }
  239. na, ok := canonicalJSON(a)
  240. if !ok {
  241. return false
  242. }
  243. nb, ok := canonicalJSON(b)
  244. if !ok {
  245. return false
  246. }
  247. return bytes.Equal(na, nb)
  248. }
  249. // canonicalJSON renders a JSON value in canonical form: sorted object keys,
  250. // no insignificant whitespace, exact number digits (json.Number). Empty
  251. // input and JSON null both canonicalize to nil.
  252. func canonicalJSON(raw json_util.RawMessage) ([]byte, bool) {
  253. if len(raw) == 0 {
  254. return nil, true
  255. }
  256. decoder := json.NewDecoder(bytes.NewReader(raw))
  257. decoder.UseNumber()
  258. var value any
  259. if err := decoder.Decode(&value); err != nil {
  260. return nil, false
  261. }
  262. if value == nil {
  263. return nil, true
  264. }
  265. out, err := json.Marshal(value)
  266. if err != nil {
  267. return nil, false
  268. }
  269. return out, true
  270. }
  271. // inboundsByTag indexes inbounds by tag; ok is false when a tag is empty or
  272. // duplicated, since such handlers can't be addressed through the API.
  273. func inboundsByTag(inbounds []InboundConfig) (map[string]*InboundConfig, bool) {
  274. byTag := make(map[string]*InboundConfig, len(inbounds))
  275. for i := range inbounds {
  276. tag := inbounds[i].Tag
  277. if tag == "" {
  278. return nil, false
  279. }
  280. if _, dup := byTag[tag]; dup {
  281. return nil, false
  282. }
  283. byTag[tag] = &inbounds[i]
  284. }
  285. return byTag, true
  286. }
  287. type outboundEntry struct {
  288. tag string
  289. raw []byte // original JSON, used for AddOutbound
  290. norm []byte // canonical JSON, used for change detection
  291. }
  292. // parseOutbounds splits the outbounds array into per-entry raw/normalized
  293. // JSON. ok is false when the array is unparsable or an entry has an empty or
  294. // duplicate tag — those can't be addressed through the API.
  295. func parseOutbounds(raw json_util.RawMessage) ([]outboundEntry, bool) {
  296. if len(raw) == 0 {
  297. return nil, true
  298. }
  299. var elems []json.RawMessage
  300. if err := json.Unmarshal(raw, &elems); err != nil {
  301. return nil, false
  302. }
  303. entries := make([]outboundEntry, 0, len(elems))
  304. seen := make(map[string]struct{}, len(elems))
  305. for _, elem := range elems {
  306. var meta struct {
  307. Tag string `json:"tag"`
  308. }
  309. if err := json.Unmarshal(elem, &meta); err != nil {
  310. return nil, false
  311. }
  312. if meta.Tag == "" {
  313. return nil, false
  314. }
  315. if _, dup := seen[meta.Tag]; dup {
  316. return nil, false
  317. }
  318. seen[meta.Tag] = struct{}{}
  319. norm, ok := canonicalJSON(json_util.RawMessage(elem))
  320. if !ok {
  321. return nil, false
  322. }
  323. entries = append(entries, outboundEntry{tag: meta.Tag, raw: elem, norm: norm})
  324. }
  325. return entries, true
  326. }
  327. // apiTagFromConfig extracts api.tag from the api section, defaulting to "api".
  328. func apiTagFromConfig(api json_util.RawMessage) string {
  329. var parsed struct {
  330. Tag string `json:"tag"`
  331. }
  332. if len(api) > 0 && json.Unmarshal(api, &parsed) == nil && parsed.Tag != "" {
  333. return parsed.Tag
  334. }
  335. return "api"
  336. }