xray.go 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "path"
  7. "path/filepath"
  8. "runtime"
  9. "strings"
  10. "sync"
  11. "github.com/mhsanaei/3x-ui/v3/internal/config"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  14. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  15. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  16. "go.uber.org/atomic"
  17. )
  18. var (
  19. p *xray.Process
  20. lock sync.Mutex
  21. isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
  22. isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
  23. result string
  24. )
  25. // XrayService provides business logic for Xray process management.
  26. // It handles starting, stopping, restarting Xray, and managing its configuration.
  27. type XrayService struct {
  28. inboundService InboundService
  29. settingService SettingService
  30. nodeService NodeService
  31. xrayAPI xray.XrayAPI
  32. }
  33. // IsXrayRunning checks if the Xray process is currently running.
  34. func (s *XrayService) IsXrayRunning() bool {
  35. return p != nil && p.IsRunning()
  36. }
  37. // XrayProcess returns the current Xray process instance (may be nil when Xray
  38. // is not running). It exposes the package-level process to callers outside this
  39. // package (e.g. the tgbot subpackage) without changing access semantics.
  40. func XrayProcess() *xray.Process {
  41. return p
  42. }
  43. // GetXrayErr returns the error from the Xray process, if any.
  44. func (s *XrayService) GetXrayErr() error {
  45. if p == nil {
  46. return nil
  47. }
  48. err := p.GetErr()
  49. if err == nil {
  50. return nil
  51. }
  52. if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
  53. // exit status 1 on Windows means that Xray process was killed
  54. // as we kill process to stop in on Windows, this is not an error
  55. return nil
  56. }
  57. return err
  58. }
  59. // GetXrayResult returns the result string from the Xray process.
  60. func (s *XrayService) GetXrayResult() string {
  61. if result != "" {
  62. return result
  63. }
  64. if s.IsXrayRunning() {
  65. return ""
  66. }
  67. if p == nil {
  68. return ""
  69. }
  70. result = p.GetResult()
  71. if runtime.GOOS == "windows" && result == "exit status 1" {
  72. // exit status 1 on Windows means that Xray process was killed
  73. // as we kill process to stop in on Windows, this is not an error
  74. return ""
  75. }
  76. return result
  77. }
  78. // GetXrayVersion returns the version of the running Xray process.
  79. func (s *XrayService) GetXrayVersion() string {
  80. if p == nil {
  81. return "Unknown"
  82. }
  83. return p.GetXrayVersion()
  84. }
  85. // RemoveIndex removes an element at the specified index from a slice.
  86. // Returns a new slice with the element removed.
  87. func RemoveIndex(s []any, index int) []any {
  88. return append(s[:index], s[index+1:]...)
  89. }
  90. // GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
  91. func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
  92. templateConfig, err := s.settingService.GetXrayConfigTemplate()
  93. if err != nil {
  94. return nil, err
  95. }
  96. xrayConfig := &xray.Config{}
  97. err = json.Unmarshal([]byte(templateConfig), xrayConfig)
  98. if err != nil {
  99. return nil, err
  100. }
  101. xrayConfig.LogConfig = resolveXrayLogPaths(xrayConfig.LogConfig)
  102. xrayConfig.API = ensureAPIServices(xrayConfig.API)
  103. xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
  104. xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
  105. // Template outbounds authored before the xray-core #6258 XHTTP rename may
  106. // still carry sessionPlacement/sessionKey; lift them too (same reason as
  107. // the per-inbound lift below).
  108. xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
  109. _, _, _ = s.inboundService.AddTraffic(nil, nil)
  110. inbounds, err := s.inboundService.GetAllInbounds()
  111. if err != nil {
  112. return nil, err
  113. }
  114. for _, inbound := range inbounds {
  115. if !inbound.Enable {
  116. continue
  117. }
  118. if inbound.NodeID != nil {
  119. continue
  120. }
  121. if inbound.Protocol == model.MTProto {
  122. continue
  123. }
  124. settings := map[string]any{}
  125. _ = json.Unmarshal([]byte(inbound.Settings), &settings)
  126. dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id)
  127. if listErr != nil {
  128. return nil, listErr
  129. }
  130. clientStats := inbound.ClientStats
  131. enableMap := make(map[string]bool, len(clientStats))
  132. for _, clientTraffic := range clientStats {
  133. enableMap[clientTraffic.Email] = clientTraffic.Enable
  134. }
  135. finalClients := make([]any, 0, len(dbClients))
  136. var wgPeers []any
  137. for i := range dbClients {
  138. c := dbClients[i]
  139. if enable, exists := enableMap[c.Email]; exists && !enable {
  140. logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c.Email)
  141. continue
  142. }
  143. if !c.Enable {
  144. continue
  145. }
  146. flow := c.Flow
  147. if flow == "xtls-rprx-vision-udp443" {
  148. flow = "xtls-rprx-vision"
  149. }
  150. entry := map[string]any{"email": c.Email}
  151. switch inbound.Protocol {
  152. case model.VLESS:
  153. if c.ID != "" {
  154. entry["id"] = c.ID
  155. }
  156. if flow != "" {
  157. entry["flow"] = flow
  158. }
  159. if c.Reverse != nil {
  160. entry["reverse"] = c.Reverse
  161. }
  162. case model.VMESS:
  163. if c.ID != "" {
  164. entry["id"] = c.ID
  165. }
  166. if c.Security != "" {
  167. entry["security"] = c.Security
  168. }
  169. case model.Trojan:
  170. if c.Password != "" {
  171. entry["password"] = c.Password
  172. }
  173. if flow != "" {
  174. entry["flow"] = flow
  175. }
  176. case model.Shadowsocks:
  177. if c.Password != "" {
  178. entry["password"] = c.Password
  179. }
  180. case model.Hysteria:
  181. if c.Auth != "" {
  182. entry["auth"] = c.Auth
  183. }
  184. case model.WireGuard:
  185. wgPeers = append(wgPeers, model.WireguardPeerFromClient(c))
  186. continue
  187. }
  188. finalClients = append(finalClients, entry)
  189. }
  190. var mutated bool
  191. if inbound.Protocol == model.WireGuard {
  192. delete(settings, "clients")
  193. if wgPeers == nil {
  194. wgPeers = []any{}
  195. }
  196. settings["peers"] = wgPeers
  197. mutated = true
  198. } else {
  199. _, hadClients := settings["clients"]
  200. mutated = hadClients || len(finalClients) > 0
  201. if mutated {
  202. settings["clients"] = finalClients
  203. }
  204. }
  205. if inboundCanHostFallbacks(inbound) {
  206. fallbacks, fbErr := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id)
  207. if fbErr != nil {
  208. return nil, fbErr
  209. }
  210. if len(fallbacks) > 0 {
  211. generic := make([]any, 0, len(fallbacks))
  212. for _, f := range fallbacks {
  213. generic = append(generic, f)
  214. }
  215. settings["fallbacks"] = generic
  216. mutated = true
  217. }
  218. }
  219. if mutated {
  220. modifiedSettings, err := json.MarshalIndent(settings, "", " ")
  221. if err != nil {
  222. return nil, err
  223. }
  224. inbound.Settings = string(modifiedSettings)
  225. }
  226. if len(inbound.StreamSettings) > 0 {
  227. // Unmarshal stream JSON
  228. var stream map[string]any
  229. _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream)
  230. // Remove the "settings" field under "tlsSettings" and "realitySettings"
  231. tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
  232. realitySettings, ok2 := stream["realitySettings"].(map[string]any)
  233. if ok1 || ok2 {
  234. if ok1 {
  235. delete(tlsSettings, "settings")
  236. } else if ok2 {
  237. delete(realitySettings, "settings")
  238. }
  239. }
  240. delete(stream, "externalProxy")
  241. // finalmask.tcp + REALITY panics Xray-core on the first connection
  242. // (XTLS/Xray-core#6453). AddInbound/UpdateInbound reject this
  243. // combination at save time, but a row saved before that guard
  244. // existed (upgrade, node sync, restored backup, direct DB edit)
  245. // would still crash Xray on the next restart without this — drop
  246. // it here too, the same way liftXhttpSessionIDKeys and
  247. // HealShadowsocksClientMethods heal other legacy data in place.
  248. if len(finalMaskRealityTcpMasks(stream)) > 0 {
  249. logger.Warningf("Inbound %q: dropping finalmask, incompatible with REALITY security (crashes Xray-core, see XTLS/Xray-core#6453)", inbound.Tag)
  250. delete(stream, "finalmask")
  251. }
  252. dropEmptyRandPackets(stream["finalmask"])
  253. if dropped := stripIncompleteXmcMasks(stream); dropped > 0 {
  254. logger.Warningf("Inbound %q: dropping %d XMC finalmask mask(s) without complete Minecraft profiles — reconfigure them to restore the obfuscation (see XTLS/Xray-core#6487)", inbound.Tag, dropped)
  255. }
  256. // xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
  257. // kept no fallback. Lift legacy sessionPlacement/sessionKey onto the
  258. // new names here so inbounds stored before the rename keep working
  259. // without the admin re-saving them.
  260. liftXhttpSessionIDKeys(stream)
  261. newStream, err := json.MarshalIndent(stream, "", " ")
  262. if err != nil {
  263. return nil, err
  264. }
  265. inbound.StreamSettings = string(newStream)
  266. }
  267. if inbound.Protocol == model.Shadowsocks {
  268. if healed, ok := model.HealShadowsocksClientMethods(inbound.Settings); ok {
  269. inbound.Settings = healed
  270. }
  271. }
  272. inboundConfig := inbound.GenXrayInboundConfig()
  273. xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
  274. }
  275. // Merge subscription-derived outbounds (if any) into the final outbounds array.
  276. // These are additive: each subscription is placed before or after the template
  277. // outbounds based on its Prepend flag, ordered by Priority. Tags assigned by the
  278. // subscription service are kept stable across refreshes so that balancers and
  279. // routing rules continue to work.
  280. subSvc := &OutboundSubscriptionService{}
  281. if prepend, appendList, err := subSvc.activeOutboundsSplit(); err == nil && (len(prepend) > 0 || len(appendList) > 0) {
  282. mergeSubscriptionOutbounds(xrayConfig, prepend, appendList)
  283. }
  284. // Route opted-in local mtproto inbounds through the core's router. Each one
  285. // gets a loopback SOCKS bridge — tagged with the inbound's own tag so it is
  286. // matchable in routing rules — that its mtg sidecar dials Telegram through.
  287. // Done after the subscription merge so a selected subscription outbound (or
  288. // balancer) is a valid rule target.
  289. for i := range inbounds {
  290. inbound := inbounds[i]
  291. if inbound.Protocol != model.MTProto || !inbound.Enable || inbound.NodeID != nil {
  292. continue
  293. }
  294. injectMtprotoEgress(xrayConfig, inbound)
  295. }
  296. // Wire the panel's own HTTP traffic through the configured outbound, after
  297. // the subscription merge so subscription outbound tags are valid targets.
  298. if egressTag, err := s.settingService.GetPanelOutbound(); err != nil {
  299. logger.Warning("read panelOutbound setting failed:", err)
  300. } else if egressTag != "" {
  301. injectPanelEgress(xrayConfig, egressTag)
  302. }
  303. nodes, err := s.nodeService.GetAll()
  304. if err != nil {
  305. logger.Warning("read nodes for egress injection failed:", err)
  306. } else {
  307. injectNodeEgresses(xrayConfig, nodes)
  308. }
  309. return xrayConfig, nil
  310. }
  311. // PanelEgressInboundTag is the tag of the loopback SOCKS inbound injected into
  312. // the generated config when a panel outbound is configured. The panel's own
  313. // HTTP clients dial through it to egress via the chosen outbound.
  314. const PanelEgressInboundTag = "panel-egress"
  315. // panelEgressBasePort is the first port tried for the egress bridge; ports
  316. // already taken by other inbounds in the generated config are skipped.
  317. const panelEgressBasePort = 62790
  318. // injectPanelEgress appends a loopback SOCKS inbound and routing rule only when
  319. // outboundTag resolves in the final outbound or balancer set. Otherwise the
  320. // entire injection is skipped. Generated state is hot-appliable and never
  321. // modifies the stored template or restarts the core.
  322. func injectPanelEgress(cfg *xray.Config, outboundTag string) {
  323. for i := range cfg.InboundConfigs {
  324. if cfg.InboundConfigs[i].Tag == PanelEgressInboundTag {
  325. logger.Warning("panel egress: inbound tag [", PanelEgressInboundTag, "] already exists, skipping injection")
  326. return
  327. }
  328. }
  329. // The rule must exist before the inbound takes traffic, otherwise the
  330. // bridge would silently egress through the default outbound instead.
  331. routing := map[string]any{}
  332. if len(cfg.RouterConfig) > 0 {
  333. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  334. logger.Warning("panel egress: routing section is unparsable, skipping injection:", err)
  335. return
  336. }
  337. }
  338. if !routingTargetExists(routing, cfg.OutboundConfigs, outboundTag) {
  339. logger.Warning("panel egress: target tag [", outboundTag, "] not found, skipping injection")
  340. return
  341. }
  342. rules, _ := routing["rules"].([]any)
  343. rule := map[string]any{
  344. "type": "field",
  345. "inboundTag": []any{PanelEgressInboundTag},
  346. }
  347. // The configured tag may name a routing balancer instead of a concrete
  348. // outbound. A field rule can target either, so emit the matching key —
  349. // balancerTag load-balances the panel's own traffic across the balancer's
  350. // outbounds, while a plain outbound tag keeps the original behavior.
  351. if routingTagIsBalancer(routing, outboundTag) {
  352. rule["balancerTag"] = outboundTag
  353. } else {
  354. rule["outboundTag"] = outboundTag
  355. }
  356. routing["rules"] = append([]any{rule}, rules...)
  357. newRouting, err := json.Marshal(routing)
  358. if err != nil {
  359. logger.Warning("panel egress: failed to rebuild routing section, skipping injection:", err)
  360. return
  361. }
  362. cfg.RouterConfig = json_util.RawMessage(newRouting)
  363. used := make(map[int]struct{}, len(cfg.InboundConfigs))
  364. for i := range cfg.InboundConfigs {
  365. used[cfg.InboundConfigs[i].Port] = struct{}{}
  366. }
  367. port := panelEgressBasePort
  368. for {
  369. if _, taken := used[port]; !taken {
  370. break
  371. }
  372. port++
  373. }
  374. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  375. Listen: json_util.RawMessage(`"127.0.0.1"`),
  376. Port: port,
  377. Protocol: "socks",
  378. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  379. Tag: PanelEgressInboundTag,
  380. })
  381. }
  382. func outboundTagExists(outbounds json_util.RawMessage, tag string) bool {
  383. var parsed []struct {
  384. Tag string `json:"tag"`
  385. }
  386. if tag == "" || json.Unmarshal(outbounds, &parsed) != nil {
  387. return false
  388. }
  389. for _, outbound := range parsed {
  390. if outbound.Tag == tag {
  391. return true
  392. }
  393. }
  394. return false
  395. }
  396. func routingTargetExists(routing map[string]any, outbounds json_util.RawMessage, tag string) bool {
  397. return routingTagIsBalancer(routing, tag) || outboundTagExists(outbounds, tag)
  398. }
  399. // NodeEgressInboundTag returns the loopback SOCKS inbound tag for a given node.
  400. func NodeEgressInboundTag(nodeID int) string {
  401. return fmt.Sprintf("node-egress-%d", nodeID)
  402. }
  403. // nodeEgressBasePort is the first port tried for node egress bridges.
  404. const nodeEgressBasePort = 62800
  405. // injectNodeEgresses appends a loopback SOCKS inbound per enabled node that has
  406. // an OutboundTag, and prepends a routing rule sending that inbound's traffic to
  407. // the selected outbound tag. These bridges are hot-appliable.
  408. func injectNodeEgresses(cfg *xray.Config, nodes []*model.Node) {
  409. routing := map[string]any{}
  410. if len(cfg.RouterConfig) > 0 {
  411. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  412. logger.Warning("node egress: routing section is unparsable, skipping injection:", err)
  413. return
  414. }
  415. }
  416. used := make(map[int]struct{}, len(cfg.InboundConfigs))
  417. usedTags := make(map[string]struct{}, len(cfg.InboundConfigs))
  418. for i := range cfg.InboundConfigs {
  419. used[cfg.InboundConfigs[i].Port] = struct{}{}
  420. usedTags[cfg.InboundConfigs[i].Tag] = struct{}{}
  421. }
  422. rules, _ := routing["rules"].([]any)
  423. newRules := make([]any, 0)
  424. for _, n := range nodes {
  425. if !n.Enable || n.OutboundTag == "" {
  426. continue
  427. }
  428. if !routingTargetExists(routing, cfg.OutboundConfigs, n.OutboundTag) {
  429. logger.Warning("node egress: target tag [", n.OutboundTag, "] not found, skipping node [", n.Id, "]")
  430. continue
  431. }
  432. tag := NodeEgressInboundTag(n.Id)
  433. if _, exists := usedTags[tag]; exists {
  434. logger.Warning("node egress: inbound tag [", tag, "] already exists, skipping")
  435. continue
  436. }
  437. usedTags[tag] = struct{}{}
  438. rule := map[string]any{
  439. "type": "field",
  440. "inboundTag": []any{tag},
  441. }
  442. if routingTagIsBalancer(routing, n.OutboundTag) {
  443. rule["balancerTag"] = n.OutboundTag
  444. } else {
  445. rule["outboundTag"] = n.OutboundTag
  446. }
  447. newRules = append(newRules, rule)
  448. port := nodeEgressBasePort + n.Id
  449. for {
  450. if _, taken := used[port]; !taken {
  451. break
  452. }
  453. port++
  454. }
  455. used[port] = struct{}{}
  456. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  457. Listen: json_util.RawMessage(`"127.0.0.1"`),
  458. Port: port,
  459. Protocol: "socks",
  460. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  461. Tag: tag,
  462. })
  463. }
  464. if len(newRules) == 0 {
  465. return
  466. }
  467. routing["rules"] = append(newRules, rules...)
  468. newRouting, err := json.Marshal(routing)
  469. if err != nil {
  470. logger.Warning("node egress: failed to rebuild routing section, skipping injection:", err)
  471. return
  472. }
  473. cfg.RouterConfig = json_util.RawMessage(newRouting)
  474. }
  475. // routingTagIsBalancer reports whether tag names a balancer in the parsed
  476. // routing section. The panel-egress rule targets a balancer via balancerTag and
  477. // a concrete outbound via outboundTag, so the caller picks the key from this.
  478. func routingTagIsBalancer(routing map[string]any, tag string) bool {
  479. if tag == "" {
  480. return false
  481. }
  482. balancers, ok := routing["balancers"].([]any)
  483. if !ok {
  484. return false
  485. }
  486. for _, b := range balancers {
  487. bm, ok := b.(map[string]any)
  488. if !ok {
  489. continue
  490. }
  491. if t, ok := bm["tag"].(string); ok && t == tag {
  492. return true
  493. }
  494. }
  495. return false
  496. }
  497. // mtprotoEgressSocksSettings is the loopback SOCKS server a routed mtproto
  498. // inbound exposes for its mtg sidecar to dial Telegram through. mtg makes plain
  499. // TCP connections, so UDP is left off (matching the panel egress bridge).
  500. const mtprotoEgressSocksSettings = `{"auth":"noauth","udp":false}`
  501. // injectMtprotoEgress wires one routed mtproto inbound into the generated
  502. // config after any selected outbound resolves in the final target set. Invalid
  503. // selected targets or routing data skip the entire injection; without a selected
  504. // outbound, the bridge retains default-route behavior. Generated state remains
  505. // hot-appliable, leaves the stored template untouched, and never forces a full
  506. // Xray restart. Mirrors injectPanelEgress.
  507. func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
  508. var parsed struct {
  509. RouteThroughXray bool `json:"routeThroughXray"`
  510. RouteXrayPort int `json:"routeXrayPort"`
  511. OutboundTag string `json:"outboundTag"`
  512. }
  513. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
  514. return
  515. }
  516. if !parsed.RouteThroughXray || parsed.RouteXrayPort <= 0 || inbound.Tag == "" {
  517. return
  518. }
  519. tag := inbound.Tag
  520. for i := range cfg.InboundConfigs {
  521. if cfg.InboundConfigs[i].Tag == tag {
  522. logger.Warning("mtproto egress: inbound tag [", tag, "] already present in generated config, skipping bridge")
  523. return
  524. }
  525. }
  526. if parsed.OutboundTag != "" {
  527. routing := map[string]any{}
  528. if len(cfg.RouterConfig) > 0 {
  529. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  530. logger.Warning("mtproto egress: routing section is unparsable, skipping injection:", err)
  531. return
  532. }
  533. }
  534. if !routingTargetExists(routing, cfg.OutboundConfigs, parsed.OutboundTag) {
  535. logger.Warning("mtproto egress: target tag [", parsed.OutboundTag, "] not found, skipping injection")
  536. return
  537. }
  538. rules, _ := routing["rules"].([]any)
  539. rule := map[string]any{
  540. "type": "field",
  541. "inboundTag": []any{tag},
  542. }
  543. if routingTagIsBalancer(routing, parsed.OutboundTag) {
  544. rule["balancerTag"] = parsed.OutboundTag
  545. } else {
  546. rule["outboundTag"] = parsed.OutboundTag
  547. }
  548. routing["rules"] = append([]any{rule}, rules...)
  549. newRouting, err := json.Marshal(routing)
  550. if err != nil {
  551. logger.Warning("mtproto egress: failed to rebuild routing section, skipping injection:", err)
  552. return
  553. }
  554. cfg.RouterConfig = json_util.RawMessage(newRouting)
  555. }
  556. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  557. Listen: json_util.RawMessage(`"127.0.0.1"`),
  558. Port: parsed.RouteXrayPort,
  559. Protocol: "socks",
  560. Settings: json_util.RawMessage(mtprotoEgressSocksSettings),
  561. Tag: tag,
  562. })
  563. }
  564. // mergeSubscriptionOutbounds appends the subscription outbounds to the
  565. // OutboundConfigs array of the xray config. It works on the already-unmarshaled
  566. // template so that manually configured outbounds are never overwritten.
  567. //
  568. // Safety: if we cannot parse the template's outbounds array, we leave
  569. // OutboundConfigs exactly as it came from the template (we do not inject
  570. // subscription outbounds). This prevents us from accidentally dropping the
  571. // user's manually configured outbounds when the template is in a weird state.
  572. func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
  573. if len(prepend) == 0 && len(appendList) == 0 {
  574. return
  575. }
  576. var templateOutbounds []any
  577. if len(cfg.OutboundConfigs) > 0 {
  578. if err := json.Unmarshal(cfg.OutboundConfigs, &templateOutbounds); err != nil {
  579. // Corrupt template outbounds — do not touch the field at all.
  580. // The user will see problems on Xray start / next save.
  581. return
  582. }
  583. }
  584. var merged []any
  585. merged = append(merged, prepend...)
  586. merged = append(merged, templateOutbounds...)
  587. merged = append(merged, appendList...)
  588. combined, err := json.MarshalIndent(merged, "", " ")
  589. if err != nil {
  590. return
  591. }
  592. cfg.OutboundConfigs = json_util.RawMessage(combined)
  593. }
  594. // ensureAPIServices guarantees the gRPC services the panel depends on are
  595. // listed in the generated config's api block: HandlerService and StatsService
  596. // have always been required for inbound/user management and traffic polling,
  597. // and RoutingService enables hot routing reload on templates saved before it
  598. // was added to the default template. The stored template itself is not
  599. // modified — only the generated runtime config.
  600. func ensureAPIServices(api json_util.RawMessage) json_util.RawMessage {
  601. if len(api) == 0 {
  602. // No api block means the panel's API integration is deliberately
  603. // disabled; don't resurrect it behind the user's back.
  604. return api
  605. }
  606. var parsed map[string]any
  607. if err := json.Unmarshal(api, &parsed); err != nil {
  608. return api
  609. }
  610. services, _ := parsed["services"].([]any)
  611. have := make(map[string]bool, len(services))
  612. for _, svc := range services {
  613. if name, ok := svc.(string); ok {
  614. have[name] = true
  615. }
  616. }
  617. added := false
  618. for _, name := range []string{"HandlerService", "StatsService", "RoutingService"} {
  619. if !have[name] {
  620. services = append(services, name)
  621. added = true
  622. }
  623. }
  624. if !added {
  625. return api
  626. }
  627. parsed["services"] = services
  628. out, err := json.Marshal(parsed)
  629. if err != nil {
  630. return api
  631. }
  632. return out
  633. }
  634. // ensureStatsPolicy guarantees every policy level in the generated config has
  635. // statsUserOnline enabled, so the core tracks per-email online IPs for the
  636. // panel's online view and access-log-free IP limiting. Generated clients carry
  637. // no explicit level, so level "0" is created when absent. The flag is panel
  638. // infrastructure and is forced on even over an explicit false in the template,
  639. // same as the api services above. An entirely missing or unparsable policy
  640. // block is left alone; the stored template itself is never modified — only the
  641. // generated runtime config.
  642. func ensureStatsPolicy(policy json_util.RawMessage) json_util.RawMessage {
  643. if len(policy) == 0 {
  644. return policy
  645. }
  646. var parsed map[string]any
  647. if err := json.Unmarshal(policy, &parsed); err != nil {
  648. return policy
  649. }
  650. levels, _ := parsed["levels"].(map[string]any)
  651. if levels == nil {
  652. levels = make(map[string]any)
  653. }
  654. if _, ok := levels["0"]; !ok {
  655. levels["0"] = map[string]any{}
  656. }
  657. changed := false
  658. for _, raw := range levels {
  659. level, ok := raw.(map[string]any)
  660. if !ok {
  661. continue
  662. }
  663. if enabled, ok := level["statsUserOnline"].(bool); !ok || !enabled {
  664. level["statsUserOnline"] = true
  665. changed = true
  666. }
  667. }
  668. if !changed {
  669. return policy
  670. }
  671. parsed["levels"] = levels
  672. out, err := json.Marshal(parsed)
  673. if err != nil {
  674. return policy
  675. }
  676. return out
  677. }
  678. func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
  679. if len(logCfg) == 0 {
  680. return logCfg
  681. }
  682. var parsed map[string]any
  683. if err := json.Unmarshal(logCfg, &parsed); err != nil {
  684. return logCfg
  685. }
  686. changed := false
  687. for _, key := range []string{"access", "error"} {
  688. v, ok := parsed[key].(string)
  689. if !ok {
  690. continue
  691. }
  692. trimmed := strings.TrimSpace(v)
  693. if trimmed == "" || strings.EqualFold(trimmed, "none") {
  694. continue
  695. }
  696. base := path.Base(filepath.ToSlash(trimmed))
  697. if base == "" || base == "." || base == ".." || base == "/" {
  698. continue
  699. }
  700. confined := filepath.Join(config.GetLogFolder(), base)
  701. if confined == trimmed {
  702. continue
  703. }
  704. parsed[key] = confined
  705. changed = true
  706. }
  707. if !changed {
  708. return logCfg
  709. }
  710. out, err := json.Marshal(parsed)
  711. if err != nil {
  712. return logCfg
  713. }
  714. return out
  715. }
  716. // stripDisabledRules removes routing rules marked `enabled: false` from the
  717. // generated runtime config and strips the panel-only `enabled` key from the
  718. // rest, since xray-core has no such field. The internal api rule is always
  719. // kept (see isApiRule) so traffic stats can't be toggled off. The stored
  720. // template is untouched — only the generated config is filtered.
  721. func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
  722. if len(routerCfg) == 0 {
  723. return routerCfg
  724. }
  725. var parsed map[string]any
  726. if err := json.Unmarshal(routerCfg, &parsed); err != nil {
  727. return routerCfg
  728. }
  729. rules, ok := parsed["rules"].([]any)
  730. if !ok || len(rules) == 0 {
  731. return routerCfg
  732. }
  733. var activeRules []any
  734. changed := false
  735. for _, rawRule := range rules {
  736. rule, ok := rawRule.(map[string]any)
  737. if !ok {
  738. activeRules = append(activeRules, rawRule)
  739. continue
  740. }
  741. if enabledRaw, exists := rule["enabled"]; exists {
  742. // The internal api rule carries traffic stats and must never be
  743. // dropped, even if it was somehow marked disabled.
  744. enabled, ok := enabledRaw.(bool)
  745. if ok && !enabled && !isApiRule(rule) {
  746. changed = true
  747. continue
  748. }
  749. delete(rule, "enabled")
  750. changed = true
  751. }
  752. activeRules = append(activeRules, rule)
  753. }
  754. if !changed {
  755. return routerCfg
  756. }
  757. parsed["rules"] = activeRules
  758. out, err := json.Marshal(parsed)
  759. if err != nil {
  760. return routerCfg
  761. }
  762. return out
  763. }
  764. // GetXrayTraffic fetches the current traffic statistics from the running Xray process.
  765. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
  766. if !s.IsXrayRunning() {
  767. err := errors.New("xray is not running")
  768. logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
  769. return nil, nil, err
  770. }
  771. apiPort := p.GetAPIPort()
  772. if err := s.xrayAPI.Init(apiPort); err != nil {
  773. logger.Debug("Failed to initialize Xray API:", err)
  774. return nil, nil, err
  775. }
  776. defer s.xrayAPI.Close()
  777. traffic, clientTraffic, err := s.xrayAPI.GetTraffic()
  778. if err != nil {
  779. logger.Debug("Failed to fetch Xray traffic:", err)
  780. return nil, nil, err
  781. }
  782. return traffic, clientTraffic, nil
  783. }
  784. // GetOnlineUsers returns connection-based online users (email + source IPs)
  785. // from the running core's online-stats API. ok=false means the API is not
  786. // available — xray isn't running or the core predates the online-stats RPCs —
  787. // and callers must use the legacy traffic-delta / access-log paths. The
  788. // capability is probed lazily per process: an Unimplemented answer pins this
  789. // core as unsupported until the next restart, while transient errors leave the
  790. // capability undecided so a flaky poll can't lock in legacy mode.
  791. func (s *XrayService) GetOnlineUsers() ([]xray.OnlineUser, bool, error) {
  792. if !s.IsXrayRunning() {
  793. return nil, false, nil
  794. }
  795. if p.OnlineAPISupport() == xray.OnlineAPIUnsupported {
  796. return nil, false, nil
  797. }
  798. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  799. logger.Debug("Failed to initialize Xray API:", err)
  800. return nil, false, err
  801. }
  802. defer s.xrayAPI.Close()
  803. users, err := s.xrayAPI.GetOnlineUsers()
  804. if err != nil {
  805. if xray.IsUnimplementedErr(err) {
  806. p.SetOnlineAPISupport(xray.OnlineAPIUnsupported)
  807. logger.Info("xray core does not support the online-stats API; falling back to traffic-delta onlines and access-log IP limit")
  808. return nil, false, nil
  809. }
  810. logger.Debug("Failed to fetch Xray online users:", err)
  811. return nil, false, err
  812. }
  813. if p.OnlineAPISupport() == xray.OnlineAPIUnknown {
  814. p.SetOnlineAPISupport(xray.OnlineAPISupported)
  815. logger.Info("xray core supports the online-stats API; using connection-based onlines and access-log-free IP limit")
  816. }
  817. return users, true, nil
  818. }
  819. // BalancerStatus is the live view of one balancer for the panel UI. Running
  820. // is false when the balancer isn't present in the running core (e.g. xray is
  821. // stopped or the balancer hasn't been saved/applied yet).
  822. type BalancerStatus struct {
  823. Tag string `json:"tag"`
  824. Running bool `json:"running"`
  825. Override string `json:"override"`
  826. Selected []string `json:"selected"`
  827. }
  828. // GetBalancersStatus queries the running core for the live state of the
  829. // given balancer tags. Per-tag failures are reported as Running=false rather
  830. // than failing the whole call, so the UI can render saved-but-not-applied
  831. // balancers alongside live ones.
  832. func (s *XrayService) GetBalancersStatus(tags []string) ([]BalancerStatus, error) {
  833. statuses := make([]BalancerStatus, 0, len(tags))
  834. if !s.IsXrayRunning() {
  835. for _, tag := range tags {
  836. statuses = append(statuses, BalancerStatus{Tag: tag})
  837. }
  838. return statuses, nil
  839. }
  840. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  841. return nil, err
  842. }
  843. defer s.xrayAPI.Close()
  844. for _, tag := range tags {
  845. info, err := s.xrayAPI.GetBalancerInfo(tag)
  846. if err != nil {
  847. logger.Debug("get balancer info [", tag, "] failed:", err)
  848. statuses = append(statuses, BalancerStatus{Tag: tag})
  849. continue
  850. }
  851. statuses = append(statuses, BalancerStatus{
  852. Tag: tag,
  853. Running: true,
  854. Override: info.Override,
  855. Selected: info.Selected,
  856. })
  857. }
  858. return statuses, nil
  859. }
  860. // OverrideBalancer forces a balancer in the running core to use the given
  861. // outbound tag; an empty target clears the override. When target names
  862. // another balancer, the override resolves to the loopback outbound that
  863. // routes traffic through the target balancer via the routing rules.
  864. func (s *XrayService) OverrideBalancer(tag, target string) error {
  865. if !s.IsXrayRunning() {
  866. return errors.New("xray is not running")
  867. }
  868. if target != "" {
  869. resolved, err := s.resolveOverrideTarget(target)
  870. if err != nil {
  871. return err
  872. }
  873. if resolved != "" {
  874. target = resolved
  875. }
  876. }
  877. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  878. return err
  879. }
  880. defer s.xrayAPI.Close()
  881. return s.xrayAPI.SetBalancerTarget(tag, target)
  882. }
  883. // resolveOverrideTarget checks if target names a balancer and, if so,
  884. // returns the loopback outbound tag that routes to it through the
  885. // routing rules. Returns empty if target is already a concrete outbound.
  886. func (s *XrayService) resolveOverrideTarget(target string) (string, error) {
  887. template, err := s.settingService.GetXrayConfigTemplate()
  888. if err != nil {
  889. return "", err
  890. }
  891. var cfg map[string]any
  892. if err := json.Unmarshal([]byte(template), &cfg); err != nil {
  893. return "", err
  894. }
  895. routing, _ := cfg["routing"].(map[string]any)
  896. if routing == nil {
  897. return "", nil
  898. }
  899. rules, _ := routing["rules"].([]any)
  900. for _, r := range rules {
  901. rule, ok := r.(map[string]any)
  902. if !ok {
  903. continue
  904. }
  905. if rule["balancerTag"] != target {
  906. continue
  907. }
  908. inboundTags, ok := rule["inboundTag"].([]any)
  909. if !ok || len(inboundTags) == 0 {
  910. continue
  911. }
  912. if lbTag, ok := inboundTags[0].(string); ok && strings.HasPrefix(lbTag, "_bl_") {
  913. return lbTag, nil
  914. }
  915. }
  916. return "", nil
  917. }
  918. // TestRoute asks the running core which outbound its router picks for the
  919. // described connection.
  920. func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
  921. if !s.IsXrayRunning() {
  922. return nil, errors.New("xray is not running")
  923. }
  924. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  925. return nil, err
  926. }
  927. defer s.xrayAPI.Close()
  928. return s.xrayAPI.TestRoute(req)
  929. }
  930. // RestartXray reconciles the running Xray process with the current desired
  931. // config. When isForce is false it first tries to apply the changes through
  932. // the Xray gRPC API without restarting the process (inbounds, outbounds and
  933. // routing rules/balancers are hot-reloadable); only changes the core cannot
  934. // take at runtime — or a force request — stop and restart the process.
  935. func (s *XrayService) RestartXray(isForce bool) error {
  936. lock.Lock()
  937. defer lock.Unlock()
  938. logger.Debug("restart Xray, force:", isForce)
  939. if !isForce && isManuallyStopped.Load() {
  940. return nil
  941. }
  942. isManuallyStopped.Store(false)
  943. xrayConfig, err := s.GetXrayConfig()
  944. if err != nil {
  945. return err
  946. }
  947. if s.IsXrayRunning() {
  948. configUnchanged := p.GetConfig().Equals(xrayConfig)
  949. if !isForce && configUnchanged && !isNeedXrayRestart.Load() {
  950. logger.Debug("It does not need to restart Xray")
  951. return nil
  952. }
  953. if !isForce && !configUnchanged && s.tryHotApply(xrayConfig) {
  954. logger.Info("Xray config changes applied through the core API, no restart needed")
  955. return nil
  956. }
  957. _ = p.Stop()
  958. }
  959. p = xray.NewProcess(xrayConfig)
  960. result = ""
  961. s.xrayAPI.StatsLastValues = nil
  962. err = p.Start()
  963. if err != nil {
  964. return err
  965. }
  966. return nil
  967. }
  968. // tryHotApply attempts to reconcile the running Xray instance with newCfg
  969. // through the core gRPC API (HandlerService for inbounds/outbounds,
  970. // RoutingService for rules/balancers). It returns true when the running
  971. // instance now matches newCfg; on any failure it returns false and the
  972. // caller falls back to a full process restart, which cleans up whatever was
  973. // partially applied. Callers must hold the package-level lock.
  974. func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
  975. oldCfg := p.GetConfig()
  976. diff, ok := xray.ComputeHotDiff(oldCfg, newCfg)
  977. if !ok {
  978. logger.Debug("hot apply: config change is not API-applicable, falling back to restart")
  979. return false
  980. }
  981. if diff.Empty() {
  982. p.SetConfig(newCfg)
  983. return true
  984. }
  985. apiPort := p.GetAPIPort()
  986. if apiPort <= 0 {
  987. return false
  988. }
  989. // A dedicated client: s.xrayAPI may be in use by traffic polling on other
  990. // service instances and is reset around restarts.
  991. hotAPI := xray.XrayAPI{}
  992. if err := hotAPI.Init(apiPort); err != nil {
  993. logger.Debug("hot apply: failed to init xray api:", err)
  994. return false
  995. }
  996. defer hotAPI.Close()
  997. // Removals first so changed handlers and port swaps never collide with
  998. // the additions that follow.
  999. for _, u := range diff.RemovedUsers {
  1000. if err := hotAPI.RemoveUser(u.Tag, u.Email); err != nil && !xray.IsMissingHandlerErr(err) {
  1001. logger.Info("hot apply: remove user [", u.Email, "] from [", u.Tag, "] failed:", err)
  1002. return false
  1003. }
  1004. }
  1005. for _, tag := range diff.RemovedInboundTags {
  1006. if err := hotAPI.DelInbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  1007. logger.Info("hot apply: remove inbound [", tag, "] failed:", err)
  1008. return false
  1009. }
  1010. }
  1011. for _, tag := range diff.RemovedOutboundTags {
  1012. if err := hotAPI.DelOutbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  1013. logger.Info("hot apply: remove outbound [", tag, "] failed:", err)
  1014. return false
  1015. }
  1016. }
  1017. for _, ob := range diff.AddedOutbounds {
  1018. if err := addOutboundReconciling(&hotAPI, ob); err != nil {
  1019. logger.Info("hot apply: add outbound failed:", err)
  1020. return false
  1021. }
  1022. }
  1023. for _, ib := range diff.AddedInbounds {
  1024. if err := addInboundReconciling(&hotAPI, ib); err != nil {
  1025. logger.Info("hot apply: add inbound failed:", err)
  1026. return false
  1027. }
  1028. }
  1029. for _, u := range diff.AddedUsers {
  1030. if err := addUserReconciling(&hotAPI, u); err != nil {
  1031. logger.Info("hot apply: add user [", u.Email, "] to [", u.Tag, "] failed:", err)
  1032. return false
  1033. }
  1034. }
  1035. if diff.RoutingConfig != nil {
  1036. if err := hotAPI.ApplyRoutingConfig(diff.RoutingConfig); err != nil {
  1037. logger.Info("hot apply: apply routing config failed:", err)
  1038. return false
  1039. }
  1040. }
  1041. p.SetConfig(newCfg)
  1042. return true
  1043. }
  1044. // addUserReconciling adds a user, and on an email conflict (the user was
  1045. // already applied through the runtime API) replaces the existing user instead.
  1046. func addUserReconciling(api *xray.XrayAPI, u xray.UserOp) error {
  1047. err := api.AddUser(u.Protocol, u.Tag, u.User)
  1048. if err == nil || !xray.IsUserExistsErr(err) {
  1049. return err
  1050. }
  1051. if delErr := api.RemoveUser(u.Tag, u.Email); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1052. return delErr
  1053. }
  1054. return api.AddUser(u.Protocol, u.Tag, u.User)
  1055. }
  1056. // addInboundReconciling adds an inbound, and on a tag conflict (the handler
  1057. // was already created through the runtime API while the stored snapshot was
  1058. // stale) replaces the existing handler instead.
  1059. func addInboundReconciling(api *xray.XrayAPI, inbound []byte) error {
  1060. err := api.AddInbound(inbound)
  1061. if err == nil || !xray.IsExistingTagErr(err) {
  1062. return err
  1063. }
  1064. var meta struct {
  1065. Tag string `json:"tag"`
  1066. }
  1067. if jsonErr := json.Unmarshal(inbound, &meta); jsonErr != nil || meta.Tag == "" {
  1068. return err
  1069. }
  1070. if delErr := api.DelInbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1071. return delErr
  1072. }
  1073. return api.AddInbound(inbound)
  1074. }
  1075. // addOutboundReconciling mirrors addInboundReconciling for outbounds.
  1076. func addOutboundReconciling(api *xray.XrayAPI, outbound []byte) error {
  1077. err := api.AddOutbound(outbound)
  1078. if err == nil || !xray.IsExistingTagErr(err) {
  1079. return err
  1080. }
  1081. var meta struct {
  1082. Tag string `json:"tag"`
  1083. }
  1084. if jsonErr := json.Unmarshal(outbound, &meta); jsonErr != nil || meta.Tag == "" {
  1085. return err
  1086. }
  1087. if delErr := api.DelOutbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1088. return delErr
  1089. }
  1090. return api.AddOutbound(outbound)
  1091. }
  1092. // StopXray stops the running Xray process.
  1093. func (s *XrayService) StopXray() error {
  1094. lock.Lock()
  1095. defer lock.Unlock()
  1096. isManuallyStopped.Store(true)
  1097. logger.Debug("Attempting to stop Xray...")
  1098. if s.IsXrayRunning() {
  1099. return p.Stop()
  1100. }
  1101. return errors.New("xray is not running")
  1102. }
  1103. // SetToNeedRestart marks that Xray needs to be restarted.
  1104. func (s *XrayService) SetToNeedRestart() {
  1105. isNeedXrayRestart.Store(true)
  1106. }
  1107. // GetXrayAPIPort returns the port the local xray process is listening on
  1108. // for its gRPC HandlerService, or 0 when xray isn't currently running.
  1109. // Exposed for the runtime package's LocalRuntime adapter — runtime can't
  1110. // reach into the package-level `p` directly without a service-package
  1111. // import cycle.
  1112. func (s *XrayService) GetXrayAPIPort() int {
  1113. if p == nil || !p.IsRunning() {
  1114. return 0
  1115. }
  1116. return p.GetAPIPort()
  1117. }
  1118. // IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
  1119. func (s *XrayService) IsNeedRestartAndSetFalse() bool {
  1120. return isNeedXrayRestart.CompareAndSwap(true, false)
  1121. }
  1122. // ApplyPendingRestart consumes the need-restart flag and restarts Xray. If the
  1123. // restart fails (for example GetXrayConfig hits a transient DB error and leaves
  1124. // the old process running), it re-arms the flag so the next tick retries instead
  1125. // of silently dropping the pending config change.
  1126. func (s *XrayService) ApplyPendingRestart() {
  1127. if !s.IsNeedRestartAndSetFalse() {
  1128. return
  1129. }
  1130. if err := s.RestartXray(false); err != nil {
  1131. logger.Error("restart xray failed:", err)
  1132. s.SetToNeedRestart()
  1133. }
  1134. }
  1135. // DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
  1136. func (s *XrayService) DidXrayCrash() bool {
  1137. return !s.IsXrayRunning() && !isManuallyStopped.Load()
  1138. }
  1139. // liftXhttpSessionIDKeys renames the legacy XHTTP session keys
  1140. // (sessionPlacement/sessionKey) to the v26.6.22 #6258 names
  1141. // (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept
  1142. // no fallback for the old names, so a config stored before the rename would be
  1143. // silently ignored by the engine. Returns true if it changed anything.
  1144. func liftXhttpSessionIDKeys(stream map[string]any) bool {
  1145. xhttp, ok := stream["xhttpSettings"].(map[string]any)
  1146. if !ok {
  1147. return false
  1148. }
  1149. changed := false
  1150. for legacy, renamed := range map[string]string{
  1151. "sessionPlacement": "sessionIDPlacement",
  1152. "sessionKey": "sessionIDKey",
  1153. } {
  1154. v, has := xhttp[legacy]
  1155. if !has {
  1156. continue
  1157. }
  1158. if _, exists := xhttp[renamed]; !exists {
  1159. xhttp[renamed] = v
  1160. }
  1161. delete(xhttp, legacy)
  1162. changed = true
  1163. }
  1164. return changed
  1165. }
  1166. // liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every
  1167. // outbound's streamSettings in the raw outbounds array. The original bytes are
  1168. // returned untouched when nothing needs lifting, so an unchanged config never
  1169. // looks modified to the hot-reload diff.
  1170. func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage {
  1171. if len(raw) == 0 {
  1172. return raw
  1173. }
  1174. var outbounds []map[string]any
  1175. if err := json.Unmarshal(raw, &outbounds); err != nil {
  1176. return raw
  1177. }
  1178. changed := false
  1179. for _, ob := range outbounds {
  1180. if stream, ok := ob["streamSettings"].(map[string]any); ok {
  1181. if liftXhttpSessionIDKeys(stream) {
  1182. changed = true
  1183. }
  1184. }
  1185. }
  1186. if !changed {
  1187. return raw
  1188. }
  1189. if rewritten, err := json.Marshal(outbounds); err == nil {
  1190. return rewritten
  1191. }
  1192. return raw
  1193. }