hot_diff.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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. RemovedUsers []UserOp
  16. AddedUsers []UserOp
  17. RemovedOutboundTags []string
  18. AddedOutbounds [][]byte
  19. RoutingConfig []byte // full new routing section; nil when unchanged
  20. }
  21. // UserOp is a per-user AlterInbound operation; User is nil for removals.
  22. type UserOp struct {
  23. Tag string
  24. Protocol string
  25. Email string
  26. User map[string]any
  27. }
  28. // Empty reports whether the diff contains no operations.
  29. func (d *HotDiff) Empty() bool {
  30. return len(d.RemovedInboundTags) == 0 &&
  31. len(d.AddedInbounds) == 0 &&
  32. len(d.RemovedUsers) == 0 &&
  33. len(d.AddedUsers) == 0 &&
  34. len(d.RemovedOutboundTags) == 0 &&
  35. len(d.AddedOutbounds) == 0 &&
  36. d.RoutingConfig == nil
  37. }
  38. // ComputeHotDiff compares two generated configs and returns the API operations
  39. // that transform a running instance from oldCfg to newCfg. ok is false when
  40. // the change touches anything that has no runtime reload API (log, dns,
  41. // policy, ...) and therefore requires a full process restart.
  42. func ComputeHotDiff(oldCfg, newCfg *Config) (*HotDiff, bool) {
  43. if oldCfg == nil || newCfg == nil {
  44. return nil, false
  45. }
  46. // Sections without a reload API must be semantically identical.
  47. // Comparison is whitespace-insensitive: a template save that merely
  48. // reformats the JSON (frontend textarea, API clients) must not be
  49. // mistaken for a real change that forces a restart.
  50. static := []struct {
  51. name string
  52. old, new json_util.RawMessage
  53. }{
  54. {"log", oldCfg.LogConfig, newCfg.LogConfig},
  55. {"dns", oldCfg.DNSConfig, newCfg.DNSConfig},
  56. {"transport", oldCfg.Transport, newCfg.Transport},
  57. {"policy", oldCfg.Policy, newCfg.Policy},
  58. {"api", oldCfg.API, newCfg.API},
  59. {"stats", oldCfg.Stats, newCfg.Stats},
  60. {"reverse", oldCfg.Reverse, newCfg.Reverse},
  61. {"fakedns", oldCfg.FakeDNS, newCfg.FakeDNS},
  62. {"observatory", oldCfg.Observatory, newCfg.Observatory},
  63. {"burstObservatory", oldCfg.BurstObservatory, newCfg.BurstObservatory},
  64. {"metrics", oldCfg.Metrics, newCfg.Metrics},
  65. {"geodata", oldCfg.Geodata, newCfg.Geodata},
  66. {"env", oldCfg.Env, newCfg.Env},
  67. }
  68. for _, section := range static {
  69. if !rawEqualNormalized(section.old, section.new) {
  70. logger.Debug("hot diff: section [", section.name, "] changed and has no reload API")
  71. return nil, false
  72. }
  73. }
  74. diff := &HotDiff{}
  75. if ok := diffInbounds(oldCfg, newCfg, diff); !ok {
  76. logger.Debug("hot diff: inbound change is not API-applicable")
  77. return nil, false
  78. }
  79. if ok := diffOutbounds(oldCfg, newCfg, diff); !ok {
  80. logger.Debug("hot diff: outbound change is not API-applicable (default outbound or tags)")
  81. return nil, false
  82. }
  83. if ok := diffRouting(oldCfg, newCfg, diff); !ok {
  84. logger.Debug("hot diff: routing change is not API-applicable (domainStrategy or section shape)")
  85. return nil, false
  86. }
  87. return diff, true
  88. }
  89. // diffInbounds fills diff with inbound removals/additions (a changed inbound
  90. // becomes remove+add). The api inbound carries the gRPC server the panel is
  91. // talking through, so any change touching it forces a restart.
  92. func diffInbounds(oldCfg, newCfg *Config, diff *HotDiff) bool {
  93. oldByTag, ok := inboundsByTag(oldCfg.InboundConfigs)
  94. if !ok {
  95. return false
  96. }
  97. newByTag, ok := inboundsByTag(newCfg.InboundConfigs)
  98. if !ok {
  99. return false
  100. }
  101. apiTag := apiTagFromConfig(newCfg.API)
  102. for i := range oldCfg.InboundConfigs {
  103. oldIb := &oldCfg.InboundConfigs[i]
  104. newIb, exists := newByTag[oldIb.Tag]
  105. if exists && inboundEqualNormalized(oldIb, newIb) {
  106. continue
  107. }
  108. if oldIb.Tag == apiTag || oldIb.Tag == "api" {
  109. return false
  110. }
  111. if exists && (inboundHasReverseClient(oldIb) || inboundHasReverseClient(newIb)) {
  112. logger.Debug("hot diff: inbound [", oldIb.Tag, "] carries a reverse-tagged client, forcing a full restart instead of a hot swap")
  113. return false
  114. }
  115. if exists && diffInboundUsers(oldIb, newIb, diff) {
  116. continue
  117. }
  118. if exists && (inboundUsesReality(oldIb) || inboundUsesReality(newIb)) {
  119. logger.Debug("hot diff: inbound [", oldIb.Tag, "] REALITY configuration changed; a gRPC remove+add does not reliably rebuild the REALITY authenticator, forcing a full restart")
  120. return false
  121. }
  122. diff.RemovedInboundTags = append(diff.RemovedInboundTags, oldIb.Tag)
  123. if exists {
  124. raw, err := json.Marshal(newIb)
  125. if err != nil {
  126. return false
  127. }
  128. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  129. }
  130. }
  131. for i := range newCfg.InboundConfigs {
  132. newIb := &newCfg.InboundConfigs[i]
  133. if _, exists := oldByTag[newIb.Tag]; exists {
  134. continue
  135. }
  136. if newIb.Tag == apiTag || newIb.Tag == "api" {
  137. return false
  138. }
  139. raw, err := json.Marshal(newIb)
  140. if err != nil {
  141. return false
  142. }
  143. diff.AddedInbounds = append(diff.AddedInbounds, raw)
  144. }
  145. return true
  146. }
  147. var userDiffableProtocols = map[string]struct{}{"vless": {}, "vmess": {}, "trojan": {}}
  148. // diffInboundUsers emits per-user AlterInbound ops when two same-tag inbounds
  149. // differ only in settings.clients, so the handler (and its listener) survives.
  150. func diffInboundUsers(oldIb, newIb *InboundConfig, diff *HotDiff) bool {
  151. if oldIb.Port != newIb.Port || oldIb.Protocol != newIb.Protocol || oldIb.Tag != newIb.Tag {
  152. return false
  153. }
  154. if _, ok := userDiffableProtocols[oldIb.Protocol]; !ok {
  155. return false
  156. }
  157. if !rawEqualNormalized(oldIb.Listen, newIb.Listen) ||
  158. !rawEqualNormalized(oldIb.StreamSettings, newIb.StreamSettings) ||
  159. !rawEqualNormalized(oldIb.Sniffing, newIb.Sniffing) {
  160. return false
  161. }
  162. oldClients, oldRest, ok := splitSettingsClients(oldIb.Settings)
  163. if !ok {
  164. return false
  165. }
  166. newClients, newRest, ok := splitSettingsClients(newIb.Settings)
  167. if !ok {
  168. return false
  169. }
  170. if !bytes.Equal(oldRest, newRest) {
  171. return false
  172. }
  173. for email, oldC := range oldClients {
  174. newC, exists := newClients[email]
  175. if exists && bytes.Equal(oldC.norm, newC.norm) {
  176. continue
  177. }
  178. diff.RemovedUsers = append(diff.RemovedUsers, UserOp{Tag: oldIb.Tag, Protocol: oldIb.Protocol, Email: email})
  179. if exists {
  180. diff.AddedUsers = append(diff.AddedUsers, UserOp{Tag: oldIb.Tag, Protocol: oldIb.Protocol, Email: email, User: newC.user})
  181. }
  182. }
  183. for email, newC := range newClients {
  184. if _, exists := oldClients[email]; !exists {
  185. diff.AddedUsers = append(diff.AddedUsers, UserOp{Tag: oldIb.Tag, Protocol: oldIb.Protocol, Email: email, User: newC.user})
  186. }
  187. }
  188. return true
  189. }
  190. type clientEntry struct {
  191. user map[string]any
  192. norm []byte
  193. }
  194. // splitSettingsClients indexes settings.clients by email and returns the rest of
  195. // the settings in canonical form; ok is false when a client has no unique email.
  196. func splitSettingsClients(raw json_util.RawMessage) (map[string]clientEntry, []byte, bool) {
  197. if len(raw) == 0 {
  198. return nil, nil, false
  199. }
  200. settings := map[string]any{}
  201. decoder := json.NewDecoder(bytes.NewReader(raw))
  202. decoder.UseNumber()
  203. if err := decoder.Decode(&settings); err != nil {
  204. return nil, nil, false
  205. }
  206. clientsRaw, hasClients := settings["clients"].([]any)
  207. if !hasClients {
  208. return nil, nil, false
  209. }
  210. clients := make(map[string]clientEntry, len(clientsRaw))
  211. for _, c := range clientsRaw {
  212. obj, ok := c.(map[string]any)
  213. if !ok {
  214. return nil, nil, false
  215. }
  216. email, _ := obj["email"].(string)
  217. if email == "" {
  218. return nil, nil, false
  219. }
  220. if _, dup := clients[email]; dup {
  221. return nil, nil, false
  222. }
  223. norm, err := json.Marshal(obj)
  224. if err != nil {
  225. return nil, nil, false
  226. }
  227. clients[email] = clientEntry{user: obj, norm: norm}
  228. }
  229. delete(settings, "clients")
  230. rest, err := json.Marshal(settings)
  231. if err != nil {
  232. return nil, nil, false
  233. }
  234. return clients, rest, true
  235. }
  236. func inboundUsesReality(ib *InboundConfig) bool {
  237. if ib == nil || len(ib.StreamSettings) == 0 {
  238. return false
  239. }
  240. var stream struct {
  241. Security string `json:"security"`
  242. }
  243. if err := json.Unmarshal(ib.StreamSettings, &stream); err != nil {
  244. return false
  245. }
  246. return stream.Security == "reality"
  247. }
  248. func inboundHasReverseClient(ib *InboundConfig) bool {
  249. if ib == nil {
  250. return false
  251. }
  252. var settings struct {
  253. Clients []struct {
  254. Reverse json.RawMessage `json:"reverse"`
  255. } `json:"clients"`
  256. }
  257. if err := json.Unmarshal(ib.Settings, &settings); err != nil {
  258. return false
  259. }
  260. for _, c := range settings.Clients {
  261. if len(c.Reverse) == 0 {
  262. continue
  263. }
  264. var tag any
  265. if err := json.Unmarshal(c.Reverse, &tag); err != nil || tag == nil {
  266. continue
  267. }
  268. return true
  269. }
  270. return false
  271. }
  272. // diffOutbounds fills diff with outbound removals/additions keyed by tag.
  273. // The first outbound is xray's default handler and the API can only append,
  274. // so any change to its identity or content forces a restart. Reordering of
  275. // the remaining outbounds is ignored — routing addresses them by tag.
  276. func diffOutbounds(oldCfg, newCfg *Config, diff *HotDiff) bool {
  277. oldOut, ok := parseOutbounds(oldCfg.OutboundConfigs)
  278. if !ok {
  279. return false
  280. }
  281. newOut, ok := parseOutbounds(newCfg.OutboundConfigs)
  282. if !ok {
  283. return false
  284. }
  285. if (len(oldOut) == 0) != (len(newOut) == 0) {
  286. return false
  287. }
  288. if len(oldOut) > 0 {
  289. if oldOut[0].tag != newOut[0].tag || !bytes.Equal(oldOut[0].norm, newOut[0].norm) {
  290. return false
  291. }
  292. }
  293. oldByTag := make(map[string]outboundEntry, len(oldOut))
  294. for _, e := range oldOut {
  295. oldByTag[e.tag] = e
  296. }
  297. newByTag := make(map[string]outboundEntry, len(newOut))
  298. for _, e := range newOut {
  299. newByTag[e.tag] = e
  300. }
  301. for _, oldE := range oldOut {
  302. newE, exists := newByTag[oldE.tag]
  303. if exists && bytes.Equal(oldE.norm, newE.norm) {
  304. continue
  305. }
  306. diff.RemovedOutboundTags = append(diff.RemovedOutboundTags, oldE.tag)
  307. if exists {
  308. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  309. }
  310. }
  311. for _, newE := range newOut {
  312. if _, exists := oldByTag[newE.tag]; !exists {
  313. diff.AddedOutbounds = append(diff.AddedOutbounds, newE.raw)
  314. }
  315. }
  316. return true
  317. }
  318. // diffRouting decides whether the routing change is limited to rules and
  319. // balancers — the only parts RoutingService.AddRule can replace at runtime.
  320. // domainStrategy/domainMatcher and any other key in the section are fixed at
  321. // process start.
  322. func diffRouting(oldCfg, newCfg *Config, diff *HotDiff) bool {
  323. if bytes.Equal(oldCfg.RouterConfig, newCfg.RouterConfig) {
  324. return true
  325. }
  326. // No routing section at start likely means no router feature (and no
  327. // RoutingService) in the running instance — only a restart can add it.
  328. if len(oldCfg.RouterConfig) == 0 || len(newCfg.RouterConfig) == 0 {
  329. return false
  330. }
  331. oldRest, ok := routingWithoutReloadable(oldCfg.RouterConfig)
  332. if !ok {
  333. return false
  334. }
  335. newRest, ok := routingWithoutReloadable(newCfg.RouterConfig)
  336. if !ok {
  337. return false
  338. }
  339. if !bytes.Equal(oldRest, newRest) {
  340. return false
  341. }
  342. diff.RoutingConfig = newCfg.RouterConfig
  343. return true
  344. }
  345. // routingWithoutReloadable returns the routing section normalized with the
  346. // runtime-reloadable keys removed, for comparing the restart-only remainder.
  347. func routingWithoutReloadable(raw []byte) ([]byte, bool) {
  348. parsed := map[string]any{}
  349. if len(raw) > 0 {
  350. decoder := json.NewDecoder(bytes.NewReader(raw))
  351. decoder.UseNumber()
  352. if err := decoder.Decode(&parsed); err != nil {
  353. return nil, false
  354. }
  355. }
  356. delete(parsed, "rules")
  357. delete(parsed, "balancers")
  358. out, err := json.Marshal(parsed)
  359. if err != nil {
  360. return nil, false
  361. }
  362. return out, true
  363. }
  364. // inboundEqualNormalized compares two inbounds ignoring JSON formatting in
  365. // their raw sections, so a reformatted template does not read as a changed
  366. // inbound.
  367. func inboundEqualNormalized(a, b *InboundConfig) bool {
  368. return a.Port == b.Port &&
  369. a.Protocol == b.Protocol &&
  370. a.Tag == b.Tag &&
  371. rawEqualNormalized(a.Listen, b.Listen) &&
  372. rawEqualNormalized(a.Settings, b.Settings) &&
  373. rawEqualNormalized(a.StreamSettings, b.StreamSettings) &&
  374. rawEqualNormalized(a.Sniffing, b.Sniffing)
  375. }
  376. // rawEqualNormalized reports whether two raw JSON values are semantically
  377. // equal: whitespace, object key order and an explicit `null` versus an
  378. // absent section are all ignored. UI editors rebuild objects on save (new
  379. // key order) and emit `null` for switched-off sections — none of that is a
  380. // reason to restart the core. Number precision is preserved via json.Number,
  381. // so genuinely different values never compare equal. Unparsable values only
  382. // compare equal byte-for-byte.
  383. func rawEqualNormalized(a, b json_util.RawMessage) bool {
  384. if bytes.Equal(a, b) {
  385. return true
  386. }
  387. na, ok := canonicalJSON(a)
  388. if !ok {
  389. return false
  390. }
  391. nb, ok := canonicalJSON(b)
  392. if !ok {
  393. return false
  394. }
  395. return bytes.Equal(na, nb)
  396. }
  397. // canonicalJSON renders a JSON value in canonical form: sorted object keys,
  398. // no insignificant whitespace, exact number digits (json.Number). Empty
  399. // input and JSON null both canonicalize to nil.
  400. func canonicalJSON(raw json_util.RawMessage) ([]byte, bool) {
  401. if len(raw) == 0 {
  402. return nil, true
  403. }
  404. decoder := json.NewDecoder(bytes.NewReader(raw))
  405. decoder.UseNumber()
  406. var value any
  407. if err := decoder.Decode(&value); err != nil {
  408. return nil, false
  409. }
  410. if value == nil {
  411. return nil, true
  412. }
  413. out, err := json.Marshal(value)
  414. if err != nil {
  415. return nil, false
  416. }
  417. return out, true
  418. }
  419. // inboundsByTag indexes inbounds by tag; ok is false when a tag is empty or
  420. // duplicated, since such handlers can't be addressed through the API.
  421. func inboundsByTag(inbounds []InboundConfig) (map[string]*InboundConfig, bool) {
  422. byTag := make(map[string]*InboundConfig, len(inbounds))
  423. for i := range inbounds {
  424. tag := inbounds[i].Tag
  425. if tag == "" {
  426. return nil, false
  427. }
  428. if _, dup := byTag[tag]; dup {
  429. return nil, false
  430. }
  431. byTag[tag] = &inbounds[i]
  432. }
  433. return byTag, true
  434. }
  435. type outboundEntry struct {
  436. tag string
  437. raw []byte // original JSON, used for AddOutbound
  438. norm []byte // canonical JSON, used for change detection
  439. }
  440. // parseOutbounds splits the outbounds array into per-entry raw/normalized
  441. // JSON. ok is false when the array is unparsable or an entry has an empty or
  442. // duplicate tag — those can't be addressed through the API.
  443. func parseOutbounds(raw json_util.RawMessage) ([]outboundEntry, bool) {
  444. if len(raw) == 0 {
  445. return nil, true
  446. }
  447. var elems []json.RawMessage
  448. if err := json.Unmarshal(raw, &elems); err != nil {
  449. return nil, false
  450. }
  451. entries := make([]outboundEntry, 0, len(elems))
  452. seen := make(map[string]struct{}, len(elems))
  453. for _, elem := range elems {
  454. var meta struct {
  455. Tag string `json:"tag"`
  456. }
  457. if err := json.Unmarshal(elem, &meta); err != nil {
  458. return nil, false
  459. }
  460. if meta.Tag == "" {
  461. return nil, false
  462. }
  463. if _, dup := seen[meta.Tag]; dup {
  464. return nil, false
  465. }
  466. seen[meta.Tag] = struct{}{}
  467. norm, ok := canonicalJSON(json_util.RawMessage(elem))
  468. if !ok {
  469. return nil, false
  470. }
  471. entries = append(entries, outboundEntry{tag: meta.Tag, raw: elem, norm: norm})
  472. }
  473. return entries, true
  474. }
  475. // apiTagFromConfig extracts api.tag from the api section, defaulting to "api".
  476. func apiTagFromConfig(api json_util.RawMessage) string {
  477. var parsed struct {
  478. Tag string `json:"tag"`
  479. }
  480. if len(api) > 0 && json.Unmarshal(api, &parsed) == nil && parsed.Tag != "" {
  481. return parsed.Tag
  482. }
  483. return "api"
  484. }