1
0

hot_diff.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. if exists && (inboundHasReverseClient(oldIb) || inboundHasReverseClient(newIb)) {
  100. logger.Debug("hot diff: inbound [", oldIb.Tag, "] carries a reverse-tagged client, forcing a full restart instead of a hot swap")
  101. return false
  102. }
  103. diff.RemovedInboundTags = append(diff.RemovedInboundTags, oldIb.Tag)
  104. if exists {
  105. raw, err := json.Marshal(newIb)
  106. if err != nil {
  107. return false
  108. }
  109. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  110. }
  111. }
  112. for i := range newCfg.InboundConfigs {
  113. newIb := &newCfg.InboundConfigs[i]
  114. if _, exists := oldByTag[newIb.Tag]; exists {
  115. continue
  116. }
  117. if newIb.Tag == apiTag || newIb.Tag == "api" {
  118. return false
  119. }
  120. raw, err := json.Marshal(newIb)
  121. if err != nil {
  122. return false
  123. }
  124. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  125. }
  126. return true
  127. }
  128. func inboundHasReverseClient(ib *InboundConfig) bool {
  129. if ib == nil {
  130. return false
  131. }
  132. var settings struct {
  133. Clients []struct {
  134. Reverse json.RawMessage `json:"reverse"`
  135. } `json:"clients"`
  136. }
  137. if err := json.Unmarshal(ib.Settings, &settings); err != nil {
  138. return false
  139. }
  140. for _, c := range settings.Clients {
  141. if len(c.Reverse) == 0 {
  142. continue
  143. }
  144. var tag any
  145. if err := json.Unmarshal(c.Reverse, &tag); err != nil || tag == nil {
  146. continue
  147. }
  148. return true
  149. }
  150. return false
  151. }
  152. // diffOutbounds fills diff with outbound removals/additions keyed by tag.
  153. // The first outbound is xray's default handler and the API can only append,
  154. // so any change to its identity or content forces a restart. Reordering of
  155. // the remaining outbounds is ignored — routing addresses them by tag.
  156. func diffOutbounds(oldCfg, newCfg *Config, diff *HotDiff) bool {
  157. oldOut, ok := parseOutbounds(oldCfg.OutboundConfigs)
  158. if !ok {
  159. return false
  160. }
  161. newOut, ok := parseOutbounds(newCfg.OutboundConfigs)
  162. if !ok {
  163. return false
  164. }
  165. if (len(oldOut) == 0) != (len(newOut) == 0) {
  166. return false
  167. }
  168. if len(oldOut) > 0 {
  169. if oldOut[0].tag != newOut[0].tag || !bytes.Equal(oldOut[0].norm, newOut[0].norm) {
  170. return false
  171. }
  172. }
  173. oldByTag := make(map[string]outboundEntry, len(oldOut))
  174. for _, e := range oldOut {
  175. oldByTag[e.tag] = e
  176. }
  177. newByTag := make(map[string]outboundEntry, len(newOut))
  178. for _, e := range newOut {
  179. newByTag[e.tag] = e
  180. }
  181. for _, oldE := range oldOut {
  182. newE, exists := newByTag[oldE.tag]
  183. if exists && bytes.Equal(oldE.norm, newE.norm) {
  184. continue
  185. }
  186. diff.RemovedOutboundTags = append(diff.RemovedOutboundTags, oldE.tag)
  187. if exists {
  188. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  189. }
  190. }
  191. for _, newE := range newOut {
  192. if _, exists := oldByTag[newE.tag]; !exists {
  193. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  194. }
  195. }
  196. return true
  197. }
  198. // diffRouting decides whether the routing change is limited to rules and
  199. // balancers — the only parts RoutingService.AddRule can replace at runtime.
  200. // domainStrategy/domainMatcher and any other key in the section are fixed at
  201. // process start.
  202. func diffRouting(oldCfg, newCfg *Config, diff *HotDiff) bool {
  203. if bytes.Equal(oldCfg.RouterConfig, newCfg.RouterConfig) {
  204. return true
  205. }
  206. // No routing section at start likely means no router feature (and no
  207. // RoutingService) in the running instance — only a restart can add it.
  208. if len(oldCfg.RouterConfig) == 0 || len(newCfg.RouterConfig) == 0 {
  209. return false
  210. }
  211. oldRest, ok := routingWithoutReloadable(oldCfg.RouterConfig)
  212. if !ok {
  213. return false
  214. }
  215. newRest, ok := routingWithoutReloadable(newCfg.RouterConfig)
  216. if !ok {
  217. return false
  218. }
  219. if !bytes.Equal(oldRest, newRest) {
  220. return false
  221. }
  222. diff.RoutingConfig = newCfg.RouterConfig
  223. return true
  224. }
  225. // routingWithoutReloadable returns the routing section normalized with the
  226. // runtime-reloadable keys removed, for comparing the restart-only remainder.
  227. func routingWithoutReloadable(raw []byte) ([]byte, bool) {
  228. parsed := map[string]any{}
  229. if len(raw) > 0 {
  230. decoder := json.NewDecoder(bytes.NewReader(raw))
  231. decoder.UseNumber()
  232. if err := decoder.Decode(&parsed); err != nil {
  233. return nil, false
  234. }
  235. }
  236. delete(parsed, "rules")
  237. delete(parsed, "balancers")
  238. out, err := json.Marshal(parsed)
  239. if err != nil {
  240. return nil, false
  241. }
  242. return out, true
  243. }
  244. // inboundEqualNormalized compares two inbounds ignoring JSON formatting in
  245. // their raw sections, so a reformatted template does not read as a changed
  246. // inbound.
  247. func inboundEqualNormalized(a, b *InboundConfig) bool {
  248. return a.Port == b.Port &&
  249. a.Protocol == b.Protocol &&
  250. a.Tag == b.Tag &&
  251. rawEqualNormalized(a.Listen, b.Listen) &&
  252. rawEqualNormalized(a.Settings, b.Settings) &&
  253. rawEqualNormalized(a.StreamSettings, b.StreamSettings) &&
  254. rawEqualNormalized(a.Sniffing, b.Sniffing)
  255. }
  256. // rawEqualNormalized reports whether two raw JSON values are semantically
  257. // equal: whitespace, object key order and an explicit `null` versus an
  258. // absent section are all ignored. UI editors rebuild objects on save (new
  259. // key order) and emit `null` for switched-off sections — none of that is a
  260. // reason to restart the core. Number precision is preserved via json.Number,
  261. // so genuinely different values never compare equal. Unparsable values only
  262. // compare equal byte-for-byte.
  263. func rawEqualNormalized(a, b json_util.RawMessage) bool {
  264. if bytes.Equal(a, b) {
  265. return true
  266. }
  267. na, ok := canonicalJSON(a)
  268. if !ok {
  269. return false
  270. }
  271. nb, ok := canonicalJSON(b)
  272. if !ok {
  273. return false
  274. }
  275. return bytes.Equal(na, nb)
  276. }
  277. // canonicalJSON renders a JSON value in canonical form: sorted object keys,
  278. // no insignificant whitespace, exact number digits (json.Number). Empty
  279. // input and JSON null both canonicalize to nil.
  280. func canonicalJSON(raw json_util.RawMessage) ([]byte, bool) {
  281. if len(raw) == 0 {
  282. return nil, true
  283. }
  284. decoder := json.NewDecoder(bytes.NewReader(raw))
  285. decoder.UseNumber()
  286. var value any
  287. if err := decoder.Decode(&value); err != nil {
  288. return nil, false
  289. }
  290. if value == nil {
  291. return nil, true
  292. }
  293. out, err := json.Marshal(value)
  294. if err != nil {
  295. return nil, false
  296. }
  297. return out, true
  298. }
  299. // inboundsByTag indexes inbounds by tag; ok is false when a tag is empty or
  300. // duplicated, since such handlers can't be addressed through the API.
  301. func inboundsByTag(inbounds []InboundConfig) (map[string]*InboundConfig, bool) {
  302. byTag := make(map[string]*InboundConfig, len(inbounds))
  303. for i := range inbounds {
  304. tag := inbounds[i].Tag
  305. if tag == "" {
  306. return nil, false
  307. }
  308. if _, dup := byTag[tag]; dup {
  309. return nil, false
  310. }
  311. byTag[tag] = &inbounds[i]
  312. }
  313. return byTag, true
  314. }
  315. type outboundEntry struct {
  316. tag string
  317. raw []byte // original JSON, used for AddOutbound
  318. norm []byte // canonical JSON, used for change detection
  319. }
  320. // parseOutbounds splits the outbounds array into per-entry raw/normalized
  321. // JSON. ok is false when the array is unparsable or an entry has an empty or
  322. // duplicate tag — those can't be addressed through the API.
  323. func parseOutbounds(raw json_util.RawMessage) ([]outboundEntry, bool) {
  324. if len(raw) == 0 {
  325. return nil, true
  326. }
  327. var elems []json.RawMessage
  328. if err := json.Unmarshal(raw, &elems); err != nil {
  329. return nil, false
  330. }
  331. entries := make([]outboundEntry, 0, len(elems))
  332. seen := make(map[string]struct{}, len(elems))
  333. for _, elem := range elems {
  334. var meta struct {
  335. Tag string `json:"tag"`
  336. }
  337. if err := json.Unmarshal(elem, &meta); err != nil {
  338. return nil, false
  339. }
  340. if meta.Tag == "" {
  341. return nil, false
  342. }
  343. if _, dup := seen[meta.Tag]; dup {
  344. return nil, false
  345. }
  346. seen[meta.Tag] = struct{}{}
  347. norm, ok := canonicalJSON(json_util.RawMessage(elem))
  348. if !ok {
  349. return nil, false
  350. }
  351. entries = append(entries, outboundEntry{tag: meta.Tag, raw: elem, norm: norm})
  352. }
  353. return entries, true
  354. }
  355. // apiTagFromConfig extracts api.tag from the api section, defaulting to "api".
  356. func apiTagFromConfig(api json_util.RawMessage) string {
  357. var parsed struct {
  358. Tag string `json:"tag"`
  359. }
  360. if len(api) > 0 && json.Unmarshal(api, &parsed) == nil && parsed.Tag != "" {
  361. return parsed.Tag
  362. }
  363. return "api"
  364. }