xray.go 41 KB

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