xray.go 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "path"
  7. "path/filepath"
  8. "runtime"
  9. "slices"
  10. "strings"
  11. "sync"
  12. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  13. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  14. "github.com/mhsanaei/3x-ui/v3/internal/config"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  17. "github.com/mhsanaei/3x-ui/v3/internal/tuic"
  18. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  19. "github.com/mhsanaei/3x-ui/v3/internal/util/maskcompat"
  20. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  21. "go.uber.org/atomic"
  22. )
  23. var (
  24. lock sync.Mutex
  25. isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
  26. isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
  27. xrayState xrayLifecycle
  28. )
  29. type xrayLifecycle struct {
  30. mu sync.RWMutex
  31. process *xray.Process
  32. result string
  33. // heldBack is why the running core still serves the previous config.
  34. heldBack string
  35. }
  36. func (s *xrayLifecycle) snapshot() (*xray.Process, string) {
  37. s.mu.RLock()
  38. defer s.mu.RUnlock()
  39. return s.process, s.result
  40. }
  41. func (s *xrayLifecycle) replace(process *xray.Process) {
  42. s.mu.Lock()
  43. s.process = process
  44. s.result = ""
  45. s.heldBack = ""
  46. s.mu.Unlock()
  47. }
  48. func (s *xrayLifecycle) holdBack(reason string) {
  49. s.mu.Lock()
  50. s.heldBack = reason
  51. s.mu.Unlock()
  52. }
  53. func (s *xrayLifecycle) heldBackReason() string {
  54. s.mu.RLock()
  55. defer s.mu.RUnlock()
  56. return s.heldBack
  57. }
  58. func (s *xrayLifecycle) storeResult(process *xray.Process, result string) {
  59. s.mu.Lock()
  60. if s.process == process && s.result == "" {
  61. s.result = result
  62. }
  63. s.mu.Unlock()
  64. }
  65. func currentXrayProcess() *xray.Process {
  66. process, _ := xrayState.snapshot()
  67. return process
  68. }
  69. // XrayService provides business logic for Xray process management.
  70. // It handles starting, stopping, restarting Xray, and managing its configuration.
  71. type XrayService struct {
  72. inboundService InboundService
  73. settingService SettingService
  74. nodeService NodeService
  75. xrayAPI xray.XrayAPI
  76. }
  77. // IsXrayRunning checks if the Xray process is currently running.
  78. func (s *XrayService) IsXrayRunning() bool {
  79. process := currentXrayProcess()
  80. return process != nil && process.IsRunning()
  81. }
  82. // XrayProcess returns the current Xray process instance (may be nil when Xray
  83. // is not running). It exposes the lifecycle snapshot to callers outside this
  84. // package (e.g. the tgbot subpackage).
  85. func XrayProcess() *xray.Process {
  86. return currentXrayProcess()
  87. }
  88. // GetXrayErr returns the error from the Xray process, if any.
  89. func (s *XrayService) GetXrayErr() error {
  90. process := currentXrayProcess()
  91. if process == nil {
  92. return nil
  93. }
  94. err := process.GetErr()
  95. if err == nil {
  96. return nil
  97. }
  98. if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
  99. // exit status 1 on Windows means that Xray process was killed
  100. // as we kill process to stop in on Windows, this is not an error
  101. return nil
  102. }
  103. return err
  104. }
  105. // GetHeldBackConfig returns why the running core still serves its previous
  106. // config, or "" when the pending config was applied.
  107. func (s *XrayService) GetHeldBackConfig() string {
  108. return xrayState.heldBackReason()
  109. }
  110. // GetXrayResult returns the result string from the Xray process.
  111. func (s *XrayService) GetXrayResult() string {
  112. process, cachedResult := xrayState.snapshot()
  113. if cachedResult != "" {
  114. return cachedResult
  115. }
  116. if process == nil || process.IsRunning() {
  117. return ""
  118. }
  119. result := process.GetResult()
  120. if runtime.GOOS == "windows" && result == "exit status 1" {
  121. // exit status 1 on Windows means that Xray process was killed
  122. // as we kill process to stop in on Windows, this is not an error
  123. return ""
  124. }
  125. xrayState.storeResult(process, result)
  126. return result
  127. }
  128. // GetXrayVersion returns the version of the running Xray process.
  129. func (s *XrayService) GetXrayVersion() string {
  130. process := currentXrayProcess()
  131. if process == nil {
  132. return "Unknown"
  133. }
  134. return process.GetXrayVersion()
  135. }
  136. // RemoveIndex removes an element at the specified index from a slice.
  137. // Returns a new slice with the element removed.
  138. func RemoveIndex(s []any, index int) []any {
  139. return append(s[:index], s[index+1:]...)
  140. }
  141. // GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
  142. func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
  143. templateConfig, err := s.settingService.GetXrayConfigTemplate()
  144. if err != nil {
  145. return nil, err
  146. }
  147. xrayConfig := &xray.Config{}
  148. err = json.Unmarshal([]byte(templateConfig), xrayConfig)
  149. if err != nil {
  150. return nil, err
  151. }
  152. xrayConfig.LogConfig = resolveXrayLogPaths(xrayConfig.LogConfig)
  153. xrayConfig.API = ensureAPIServices(xrayConfig.API)
  154. xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
  155. xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
  156. // A pasted or restored template can carry pre-#6258 XHTTP keys, a pre-26.10.10 xdns mask or a
  157. // pre-26.9.9 quicParams.udpHop, which the core ignores, or a udphop not last, which it cannot dial.
  158. xrayConfig.OutboundConfigs = healOutboundStreams(xrayConfig.OutboundConfigs,
  159. liftXhttpSessionIDKeys, upgradeStreamLegacyXdns, upgradeStreamLegacyUDPHop, normalizeStreamUDPHop)
  160. // Bridge amneziawg outbounds before anything else reads OutboundConfigs;
  161. // the core has no amneziawg proxy and would reject the raw entry.
  162. if err := transformAmneziaWGOutbounds(xrayConfig); err != nil {
  163. return nil, err
  164. }
  165. _, _, _ = s.inboundService.AddTraffic(nil, nil)
  166. inbounds, err := s.inboundService.GetAllInbounds()
  167. if err != nil {
  168. return nil, err
  169. }
  170. for _, inbound := range inbounds {
  171. if !inbound.Enable {
  172. continue
  173. }
  174. if inbound.NodeID != nil {
  175. continue
  176. }
  177. if inbound.Protocol == model.MTProto || inbound.Protocol == model.AmneziaWG || inbound.Protocol == model.TUIC {
  178. continue
  179. }
  180. settings := map[string]any{}
  181. _ = json.Unmarshal([]byte(inbound.Settings), &settings)
  182. flowAllowed := !inbound.DisableFlow &&
  183. inboundCanEnableTlsFlow(string(inbound.Protocol), inbound.StreamSettings, inbound.Settings)
  184. var wireguardClientsByEmail map[string]model.Client
  185. if inbound.Protocol == model.WireGuard {
  186. inboundClients, _ := ParseInboundSettingsClients(inbound.Settings)
  187. if len(inboundClients) > 0 {
  188. wireguardClientsByEmail = make(map[string]model.Client, len(inboundClients))
  189. for _, client := range inboundClients {
  190. wireguardClientsByEmail[strings.ToLower(strings.TrimSpace(client.Email))] = client
  191. }
  192. }
  193. }
  194. dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id)
  195. if listErr != nil {
  196. return nil, listErr
  197. }
  198. clientStats := inbound.ClientStats
  199. enableMap := make(map[string]bool, len(clientStats))
  200. for _, clientTraffic := range clientStats {
  201. enableMap[clientTraffic.Email] = clientTraffic.Enable
  202. }
  203. finalClients := make([]any, 0, len(dbClients))
  204. var wgPeers []any
  205. for i := range dbClients {
  206. c := dbClients[i]
  207. if enable, exists := enableMap[c.Email]; exists && !enable {
  208. logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c.Email)
  209. continue
  210. }
  211. if !c.Enable {
  212. continue
  213. }
  214. flow := c.Flow
  215. if flow == "xtls-rprx-vision-udp443" {
  216. flow = "xtls-rprx-vision"
  217. }
  218. if !flowAllowed {
  219. flow = ""
  220. }
  221. entry := map[string]any{"email": c.Email}
  222. switch inbound.Protocol {
  223. case model.VLESS:
  224. if c.ID != "" {
  225. entry["id"] = c.ID
  226. }
  227. if flow != "" {
  228. entry["flow"] = flow
  229. }
  230. if c.Reverse != nil {
  231. entry["reverse"] = c.Reverse
  232. }
  233. case model.VMESS:
  234. if c.ID != "" {
  235. entry["id"] = c.ID
  236. }
  237. if c.Security != "" {
  238. entry["security"] = c.Security
  239. }
  240. case model.Trojan:
  241. if c.Password != "" {
  242. entry["password"] = c.Password
  243. }
  244. if flow != "" {
  245. entry["flow"] = flow
  246. }
  247. case model.Shadowsocks:
  248. if c.Password != "" {
  249. entry["password"] = c.Password
  250. }
  251. case model.Hysteria:
  252. if c.Auth != "" {
  253. entry["auth"] = c.Auth
  254. }
  255. case model.MASQUE:
  256. if c.Password != "" {
  257. entry["password"] = c.Password
  258. }
  259. case model.WireGuard:
  260. if inboundClient, ok := wireguardClientsByEmail[strings.ToLower(strings.TrimSpace(c.Email))]; ok {
  261. c.AllowedIPs = inboundClient.AllowedIPs
  262. c.PreSharedKey = inboundClient.PreSharedKey
  263. c.PublicKey = inboundClient.PublicKey
  264. // #6731: a nil settings keepalive must not keep the other tunnel's value.
  265. if inboundClient.KeepAlive == nil {
  266. c.KeepAlive = nil
  267. } else {
  268. keepalive := *inboundClient.KeepAlive
  269. c.KeepAlive = &keepalive
  270. }
  271. }
  272. wgPeers = append(wgPeers, model.WireguardPeerFromClient(c))
  273. continue
  274. }
  275. finalClients = append(finalClients, entry)
  276. }
  277. var mutated bool
  278. if inbound.Protocol == model.WireGuard {
  279. delete(settings, "clients")
  280. if wgPeers == nil {
  281. wgPeers = []any{}
  282. }
  283. settings["peers"] = wgPeers
  284. mutated = true
  285. } else {
  286. _, hadClients := settings["clients"]
  287. mutated = hadClients || len(finalClients) > 0
  288. if mutated {
  289. settings["clients"] = finalClients
  290. }
  291. }
  292. if inboundCanHostFallbacks(inbound) {
  293. fallbacks, fbErr := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id)
  294. if fbErr != nil {
  295. return nil, fbErr
  296. }
  297. if len(fallbacks) > 0 {
  298. generic := make([]any, 0, len(fallbacks))
  299. for _, f := range fallbacks {
  300. generic = append(generic, f)
  301. }
  302. settings["fallbacks"] = generic
  303. mutated = true
  304. }
  305. }
  306. if mutated {
  307. modifiedSettings, err := json.MarshalIndent(settings, "", " ")
  308. if err != nil {
  309. return nil, err
  310. }
  311. inbound.Settings = string(modifiedSettings)
  312. }
  313. if len(inbound.StreamSettings) > 0 {
  314. // Unmarshal stream JSON
  315. var stream map[string]any
  316. _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream)
  317. // Remove the "settings" field under "tlsSettings" and "realitySettings"
  318. tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
  319. realitySettings, ok2 := stream["realitySettings"].(map[string]any)
  320. if ok1 || ok2 {
  321. if ok1 {
  322. delete(tlsSettings, "settings")
  323. } else if ok2 {
  324. delete(realitySettings, "settings")
  325. }
  326. }
  327. delete(stream, "externalProxy")
  328. // finalmask.tcp + REALITY panics Xray-core on the first connection
  329. // (XTLS/Xray-core#6453). AddInbound/UpdateInbound reject this
  330. // combination at save time, but a row saved before that guard
  331. // existed (upgrade, node sync, restored backup, direct DB edit)
  332. // would still crash Xray on the next restart without this — drop
  333. // it here too, the same way liftXhttpSessionIDKeys and
  334. // HealShadowsocksClientMethods heal other legacy data in place.
  335. if len(finalMaskRealityTcpMasks(stream)) > 0 {
  336. logger.Warningf("Inbound %q: dropping finalmask, incompatible with REALITY security (crashes Xray-core, see XTLS/Xray-core#6453)", inbound.Tag)
  337. delete(stream, "finalmask")
  338. }
  339. dropEmptyRandPackets(stream["finalmask"])
  340. if dropped := stripIncompleteXmcMasks(stream); dropped > 0 {
  341. 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)
  342. }
  343. // A row that skipped the save path can still carry an xdns shape older than 26.10.10.
  344. maskcompat.UpgradeLegacyXdns(stream["finalmask"])
  345. // xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
  346. // kept no fallback. Lift legacy sessionPlacement/sessionKey onto the
  347. // new names here so inbounds stored before the rename keep working
  348. // without the admin re-saving them.
  349. liftXhttpSessionIDKeys(stream)
  350. newStream, err := json.MarshalIndent(stream, "", " ")
  351. if err != nil {
  352. return nil, err
  353. }
  354. inbound.StreamSettings = string(newStream)
  355. }
  356. if inbound.Protocol == model.Shadowsocks {
  357. if healed, ok := model.HealShadowsocksClientMethods(inbound.Settings); ok {
  358. inbound.Settings = healed
  359. }
  360. }
  361. inboundConfig := inbound.GenXrayInboundConfig()
  362. xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
  363. }
  364. // Merge subscription-derived outbounds (if any) into the final outbounds array.
  365. // These are additive: each subscription is placed before or after the template
  366. // outbounds based on its Prepend flag, ordered by Priority. Tags assigned by the
  367. // subscription service are kept stable across refreshes so that balancers and
  368. // routing rules continue to work.
  369. subSvc := &OutboundSubscriptionService{}
  370. if prepend, appendList, err := subSvc.activeOutboundsSplit(); err == nil && (len(prepend) > 0 || len(appendList) > 0) {
  371. mergeSubscriptionOutbounds(xrayConfig, prepend, appendList)
  372. }
  373. // Route opted-in local mtproto inbounds through the core's router. Each one
  374. // gets a loopback SOCKS bridge — tagged with the inbound's own tag so it is
  375. // matchable in routing rules — that its mtg sidecar dials Telegram through.
  376. // Done after the subscription merge so a selected subscription outbound (or
  377. // balancer) is a valid rule target.
  378. for i := range inbounds {
  379. inbound := inbounds[i]
  380. if inbound.Protocol != model.MTProto || !inbound.Enable || inbound.NodeID != nil {
  381. continue
  382. }
  383. injectMtprotoEgress(xrayConfig, inbound)
  384. }
  385. // Every AmneziaWG inbound is embedded (internal/amneziawgnet: amneziawg-go
  386. // over a gVisor netstack, no kernel module) and relays every peer's
  387. // decapsulated traffic into its own loopback SOCKS5 inbound, always on —
  388. // unlike mtproto's bridge above, there's no opt-in gate here: once
  389. // traffic is decapsulated in gVisor, Xray's own freedom outbound is the
  390. // only way it reaches the real internet at all, not an optional extra
  391. // hop. Whether it goes anywhere beyond Xray's default routing is up to
  392. // whatever rules the admin adds through the stock Routing page, exactly
  393. // like routing any other protocol.
  394. injectAmneziawgnetSocks(xrayConfig, inbounds)
  395. injectTuicSocks(xrayConfig, inbounds)
  396. // Restores each opted-in peer's own distinct public IPv6 source identity
  397. // for its outbound connections — a peer that has an IPv6 address in its
  398. // AllowedIPs, on an inbound with IPv6Enabled, gets its own freedom
  399. // outbound bound to that exact address via sendThrough.
  400. // internal/amneziawgnet's own Manager is responsible for actually
  401. // aliasing that address onto the host (see v6alias.go) so the kernel
  402. // lets Xray bind an egress socket to it at all; this call only builds
  403. // the Xray-side outbound/routing-rule half.
  404. injectAmneziawgV6Egress(xrayConfig, inbounds)
  405. // Wire the panel's own HTTP traffic through the configured outbound, after
  406. // the subscription merge so subscription outbound tags are valid targets.
  407. if egressTag, err := s.settingService.GetPanelOutbound(); err != nil {
  408. logger.Warning("read panelOutbound setting failed:", err)
  409. } else if egressTag != "" {
  410. injectPanelEgress(xrayConfig, egressTag)
  411. }
  412. nodes, err := s.nodeService.GetAll()
  413. if err != nil {
  414. logger.Warning("read nodes for egress injection failed:", err)
  415. } else {
  416. injectNodeEgresses(xrayConfig, nodes)
  417. }
  418. return xrayConfig, nil
  419. }
  420. // PanelEgressInboundTag is the tag of the loopback SOCKS inbound injected into
  421. // the generated config when a panel outbound is configured. The panel's own
  422. // HTTP clients dial through it to egress via the chosen outbound.
  423. const PanelEgressInboundTag = "panel-egress"
  424. // panelEgressBasePort is the first port tried for the egress bridge; ports
  425. // already taken by other inbounds in the generated config are skipped.
  426. const panelEgressBasePort = 62790
  427. // injectPanelEgress appends a loopback SOCKS inbound and routing rule only when
  428. // outboundTag resolves in the final outbound or balancer set. Otherwise the
  429. // entire injection is skipped. Generated state is hot-appliable and never
  430. // modifies the stored template or restarts the core.
  431. func injectPanelEgress(cfg *xray.Config, outboundTag string) {
  432. for i := range cfg.InboundConfigs {
  433. if cfg.InboundConfigs[i].Tag == PanelEgressInboundTag {
  434. logger.Warning("panel egress: inbound tag [", PanelEgressInboundTag, "] already exists, skipping injection")
  435. return
  436. }
  437. }
  438. // The rule must exist before the inbound takes traffic, otherwise the
  439. // bridge would silently egress through the default outbound instead.
  440. routing := map[string]any{}
  441. if len(cfg.RouterConfig) > 0 {
  442. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  443. logger.Warning("panel egress: routing section is unparsable, skipping injection:", err)
  444. return
  445. }
  446. }
  447. if !routingTargetExists(routing, cfg.OutboundConfigs, outboundTag) {
  448. logger.Warning("panel egress: target tag [", outboundTag, "] not found, skipping injection")
  449. return
  450. }
  451. rules, _ := routing["rules"].([]any)
  452. rule := map[string]any{
  453. "type": "field",
  454. "inboundTag": []any{PanelEgressInboundTag},
  455. }
  456. // The configured tag may name a routing balancer instead of a concrete
  457. // outbound. A field rule can target either, so emit the matching key —
  458. // balancerTag load-balances the panel's own traffic across the balancer's
  459. // outbounds, while a plain outbound tag keeps the original behavior.
  460. if routingTagIsBalancer(routing, outboundTag) {
  461. rule["balancerTag"] = outboundTag
  462. } else {
  463. rule["outboundTag"] = outboundTag
  464. }
  465. routing["rules"] = append([]any{rule}, rules...)
  466. newRouting, err := json.Marshal(routing)
  467. if err != nil {
  468. logger.Warning("panel egress: failed to rebuild routing section, skipping injection:", err)
  469. return
  470. }
  471. cfg.RouterConfig = json_util.RawMessage(newRouting)
  472. used := make(map[int]struct{}, len(cfg.InboundConfigs))
  473. for i := range cfg.InboundConfigs {
  474. used[cfg.InboundConfigs[i].Port] = struct{}{}
  475. }
  476. port := panelEgressBasePort
  477. for {
  478. if _, taken := used[port]; !taken {
  479. break
  480. }
  481. port++
  482. }
  483. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  484. Listen: json_util.RawMessage(`"127.0.0.1"`),
  485. Port: port,
  486. Protocol: "socks",
  487. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  488. Tag: PanelEgressInboundTag,
  489. })
  490. }
  491. func outboundTagExists(outbounds json_util.RawMessage, tag string) bool {
  492. var parsed []struct {
  493. Tag string `json:"tag"`
  494. }
  495. if tag == "" || json.Unmarshal(outbounds, &parsed) != nil {
  496. return false
  497. }
  498. for _, outbound := range parsed {
  499. if outbound.Tag == tag {
  500. return true
  501. }
  502. }
  503. return false
  504. }
  505. func routingTargetExists(routing map[string]any, outbounds json_util.RawMessage, tag string) bool {
  506. return routingTagIsBalancer(routing, tag) || outboundTagExists(outbounds, tag)
  507. }
  508. // NodeEgressInboundTag returns the loopback SOCKS inbound tag for a given node.
  509. func NodeEgressInboundTag(nodeID int) string {
  510. return fmt.Sprintf("node-egress-%d", nodeID)
  511. }
  512. // nodeEgressBasePort is the first port tried for node egress bridges.
  513. const nodeEgressBasePort = 62800
  514. // injectNodeEgresses appends a loopback SOCKS inbound per enabled node that has
  515. // an OutboundTag, and prepends a routing rule sending that inbound's traffic to
  516. // the selected outbound tag. These bridges are hot-appliable.
  517. func injectNodeEgresses(cfg *xray.Config, nodes []*model.Node) {
  518. routing := map[string]any{}
  519. if len(cfg.RouterConfig) > 0 {
  520. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  521. logger.Warning("node egress: routing section is unparsable, skipping injection:", err)
  522. return
  523. }
  524. }
  525. used := make(map[int]struct{}, len(cfg.InboundConfigs))
  526. usedTags := make(map[string]struct{}, len(cfg.InboundConfigs))
  527. for i := range cfg.InboundConfigs {
  528. used[cfg.InboundConfigs[i].Port] = struct{}{}
  529. usedTags[cfg.InboundConfigs[i].Tag] = struct{}{}
  530. }
  531. rules, _ := routing["rules"].([]any)
  532. newRules := make([]any, 0)
  533. for _, n := range nodes {
  534. if !n.Enable || n.OutboundTag == "" {
  535. continue
  536. }
  537. if !routingTargetExists(routing, cfg.OutboundConfigs, n.OutboundTag) {
  538. logger.Warning("node egress: target tag [", n.OutboundTag, "] not found, skipping node [", n.Id, "]")
  539. continue
  540. }
  541. tag := NodeEgressInboundTag(n.Id)
  542. if _, exists := usedTags[tag]; exists {
  543. logger.Warning("node egress: inbound tag [", tag, "] already exists, skipping")
  544. continue
  545. }
  546. usedTags[tag] = struct{}{}
  547. rule := map[string]any{
  548. "type": "field",
  549. "inboundTag": []any{tag},
  550. }
  551. if routingTagIsBalancer(routing, n.OutboundTag) {
  552. rule["balancerTag"] = n.OutboundTag
  553. } else {
  554. rule["outboundTag"] = n.OutboundTag
  555. }
  556. newRules = append(newRules, rule)
  557. port := nodeEgressBasePort + n.Id
  558. for {
  559. if _, taken := used[port]; !taken {
  560. break
  561. }
  562. port++
  563. }
  564. used[port] = struct{}{}
  565. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  566. Listen: json_util.RawMessage(`"127.0.0.1"`),
  567. Port: port,
  568. Protocol: "socks",
  569. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  570. Tag: tag,
  571. })
  572. }
  573. if len(newRules) == 0 {
  574. return
  575. }
  576. routing["rules"] = append(newRules, rules...)
  577. newRouting, err := json.Marshal(routing)
  578. if err != nil {
  579. logger.Warning("node egress: failed to rebuild routing section, skipping injection:", err)
  580. return
  581. }
  582. cfg.RouterConfig = json_util.RawMessage(newRouting)
  583. }
  584. // routingTagIsBalancer reports whether tag names a balancer in the parsed
  585. // routing section. The panel-egress rule targets a balancer via balancerTag and
  586. // a concrete outbound via outboundTag, so the caller picks the key from this.
  587. func routingTagIsBalancer(routing map[string]any, tag string) bool {
  588. if tag == "" {
  589. return false
  590. }
  591. balancers, ok := routing["balancers"].([]any)
  592. if !ok {
  593. return false
  594. }
  595. for _, b := range balancers {
  596. bm, ok := b.(map[string]any)
  597. if !ok {
  598. continue
  599. }
  600. if t, ok := bm["tag"].(string); ok && t == tag {
  601. return true
  602. }
  603. }
  604. return false
  605. }
  606. // mtprotoEgressSocksSettings is the loopback SOCKS server a routed mtproto
  607. // inbound exposes for its mtg sidecar to dial Telegram through. mtg makes plain
  608. // TCP connections, so UDP is left off (matching the panel egress bridge).
  609. const mtprotoEgressSocksSettings = `{"auth":"noauth","udp":false}`
  610. // injectMtprotoEgress wires one routed mtproto inbound into the generated
  611. // config after any selected outbound resolves in the final target set. Invalid
  612. // selected targets or routing data skip the entire injection; without a selected
  613. // outbound, the bridge retains default-route behavior. Generated state remains
  614. // hot-appliable, leaves the stored template untouched, and never forces a full
  615. // Xray restart. Mirrors injectPanelEgress.
  616. func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
  617. var parsed struct {
  618. RouteThroughXray bool `json:"routeThroughXray"`
  619. RouteXrayPort int `json:"routeXrayPort"`
  620. OutboundTag string `json:"outboundTag"`
  621. }
  622. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
  623. return
  624. }
  625. if !parsed.RouteThroughXray || parsed.RouteXrayPort <= 0 || inbound.Tag == "" {
  626. return
  627. }
  628. tag := inbound.Tag
  629. for i := range cfg.InboundConfigs {
  630. if cfg.InboundConfigs[i].Tag == tag {
  631. logger.Warning("mtproto egress: inbound tag [", tag, "] already present in generated config, skipping bridge")
  632. return
  633. }
  634. }
  635. if parsed.OutboundTag != "" {
  636. routing := map[string]any{}
  637. if len(cfg.RouterConfig) > 0 {
  638. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  639. logger.Warning("mtproto egress: routing section is unparsable, skipping injection:", err)
  640. return
  641. }
  642. }
  643. if !routingTargetExists(routing, cfg.OutboundConfigs, parsed.OutboundTag) {
  644. logger.Warning("mtproto egress: target tag [", parsed.OutboundTag, "] not found, skipping injection")
  645. return
  646. }
  647. rules, _ := routing["rules"].([]any)
  648. rule := map[string]any{
  649. "type": "field",
  650. "inboundTag": []any{tag},
  651. }
  652. if routingTagIsBalancer(routing, parsed.OutboundTag) {
  653. rule["balancerTag"] = parsed.OutboundTag
  654. } else {
  655. rule["outboundTag"] = parsed.OutboundTag
  656. }
  657. routing["rules"] = append([]any{rule}, rules...)
  658. newRouting, err := json.Marshal(routing)
  659. if err != nil {
  660. logger.Warning("mtproto egress: failed to rebuild routing section, skipping injection:", err)
  661. return
  662. }
  663. cfg.RouterConfig = json_util.RawMessage(newRouting)
  664. }
  665. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  666. Listen: json_util.RawMessage(`"127.0.0.1"`),
  667. Port: parsed.RouteXrayPort,
  668. Protocol: "socks",
  669. Settings: json_util.RawMessage(mtprotoEgressSocksSettings),
  670. Tag: tag,
  671. })
  672. }
  673. // Peers resolve DNS inside the tunnel, so domain rules match only via sniffing; routeOnly
  674. // keeps the dial on the peer's IP, else Telegram's FakeTLS (IP + foreign SNI) breaks.
  675. const amneziawgEgressSniffingSettings = `{"enabled":true,"destOverride":["http","tls","quic","fakedns"],"routeOnly":true}`
  676. // injectAmneziawgnetSocks gives every enabled AmneziaWG inbound with at
  677. // least one qualifying peer its own loopback SOCKS5 inbound for the
  678. // embedded (amneziawg-go) relay path (internal/amneziawgnet) -- always on,
  679. // since there is no alternative datapath once traffic is decapsulated in
  680. // gVisor: Xray's own freedom outbound is how it reaches the real internet at
  681. // all (see internal/amneziawgnet/relay.go's doc comment, Finding 3 of the
  682. // migration plan). Tagged with the inbound's own real tag: it's already
  683. // selectable in the panel's stock Routing page (InboundService.GetInboundTags
  684. // is protocol-blind), and per-inbound traffic totals
  685. // (internal/web/service/inbound_traffic.go's addClientTraffic) match by
  686. // exact tag -- reusing it isn't a style choice.
  687. func injectAmneziawgnetSocks(cfg *xray.Config, inbounds []*model.Inbound) {
  688. existingTags := make(map[string]struct{}, len(cfg.InboundConfigs))
  689. for i := range cfg.InboundConfigs {
  690. existingTags[cfg.InboundConfigs[i].Tag] = struct{}{}
  691. }
  692. for _, inbound := range inbounds {
  693. if inbound.Protocol != model.AmneziaWG || !inbound.Enable || inbound.NodeID != nil {
  694. continue
  695. }
  696. inst, ok := amneziawg.InstanceFromInbound(inbound)
  697. if !ok {
  698. continue
  699. }
  700. if _, taken := existingTags[inbound.Tag]; taken {
  701. logger.Warning("amneziawgnet socks: inbound tag [", inbound.Tag, "] already present in generated config, skipping its relay inbound")
  702. continue
  703. }
  704. emails := make([]string, 0, len(inst.Peers))
  705. for _, p := range inst.Peers {
  706. if p.Email != "" {
  707. emails = append(emails, p.Email)
  708. }
  709. }
  710. if len(emails) == 0 {
  711. continue
  712. }
  713. settings, err := amneziawgnet.SocksInboundSettings(emails, amneziawgnet.SocksPassword())
  714. if err != nil {
  715. logger.Warning("amneziawgnet socks: building settings for inbound [", inbound.Tag, "]: ", err)
  716. continue
  717. }
  718. existingTags[inbound.Tag] = struct{}{}
  719. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  720. Listen: json_util.RawMessage(`"127.0.0.1"`),
  721. Port: amneziawgnet.SOCKSPortForInbound(inbound.Id),
  722. Protocol: "socks",
  723. Settings: json_util.RawMessage(settings),
  724. Sniffing: json_util.RawMessage(amneziawgEgressSniffingSettings),
  725. Tag: inbound.Tag,
  726. })
  727. }
  728. }
  729. const (
  730. tuicEgressSocksSettings = `{"auth":"noauth","udp":true}`
  731. // TUIC clients can connect to an IP while TLS carries a different or unresolvable SNI.
  732. // Keep sniffed domains available for routing without replacing the requested destination.
  733. tuicEgressSniffingSettings = `{"enabled":true,"destOverride":["http","tls","quic","fakedns"],"routeOnly":true}`
  734. )
  735. func injectTuicSocks(cfg *xray.Config, inbounds []*model.Inbound) {
  736. existingTags := make(map[string]struct{}, len(cfg.InboundConfigs))
  737. for i := range cfg.InboundConfigs {
  738. existingTags[cfg.InboundConfigs[i].Tag] = struct{}{}
  739. }
  740. for _, inbound := range inbounds {
  741. if inbound.Protocol != model.TUIC || !inbound.Enable || inbound.NodeID != nil {
  742. continue
  743. }
  744. if _, taken := existingTags[inbound.Tag]; taken {
  745. logger.Warning("tuic socks: inbound tag [", inbound.Tag, "] already present in generated config, skipping its relay inbound")
  746. continue
  747. }
  748. existingTags[inbound.Tag] = struct{}{}
  749. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  750. Listen: json_util.RawMessage(`"127.0.0.1"`),
  751. Port: tuic.SOCKSPortForInbound(inbound.Id),
  752. Protocol: "socks",
  753. Settings: json_util.RawMessage(tuicEgressSocksSettings),
  754. Sniffing: json_util.RawMessage(tuicEgressSniffingSettings),
  755. Tag: inbound.Tag,
  756. })
  757. }
  758. }
  759. // amneziawgV6EgressTag returns the stable, globally-unique freedom outbound
  760. // tag for one peer's IPv6 source-identity egress. Stable across config
  761. // regenerations (a pure function of two stable identifiers), so
  762. // internal/xray/hot_diff.go's tag-keyed outbound/routing diffing recognizes
  763. // "unchanged" rather than remove+recreate on every poll. The inbound.Id
  764. // prefix is defense in depth, not load-bearing on its own: email is already
  765. // enforced globally unique across the whole panel's client table
  766. // (model.ClientRecord.Email has a gorm uniqueIndex) — kept anyway since it
  767. // costs nothing and makes the tag self-describing, matching
  768. // NodeEgressInboundTag's own style.
  769. func amneziawgV6EgressTag(inboundID int, email string) string {
  770. return fmt.Sprintf("amneziawg-v6-%d-%s", inboundID, email)
  771. }
  772. // injectAmneziawgV6Egress gives every enabled, non-node-hosted AmneziaWG
  773. // peer with an IPv6 AllowedIPs entry its own single-purpose freedom
  774. // outbound, bound via sendThrough to that exact address, plus a routing
  775. // rule sending only that peer's IPv6-destined traffic through it — restoring the
  776. // per-client public IPv6 identity the hard cutover temporarily dropped
  777. // (Phase 3.5 of the migration plan). Scoped to outbound source identity
  778. // only: it depends on internal/amneziawgnet's own alias mechanism actually
  779. // giving the host that address at the OS level (see v6alias.go's
  780. // V6AliasesActive, the exact same gate this function uses below) — without
  781. // that, sendThrough fails to bind and every connection through it errors
  782. // outright (freedom.go's dial failure); there is no fallback outbound.
  783. //
  784. // The routing rule matches both inboundTag and user: SocksInboundSettings
  785. // (used by injectAmneziawgnetSocks above) already authenticates each
  786. // connection as the peer's own email via stock SOCKS5 auth, and a stock
  787. // Xray SOCKS5 inbound sets that connection's stats/routing identity from
  788. // the authenticated username — so "user" reliably isolates exactly one
  789. // peer's traffic, the same building block Finding 3 of the migration plan
  790. // already established for per-client stats.
  791. //
  792. // Modeled on injectNodeEgresses (the established N-per-slice inbound+rule
  793. // precedent, not injectAmneziawgnetSocks itself, which only ever emits a
  794. // single inbound and never touches outbounds/routing) and
  795. // mergeSubscriptionOutbounds's unmarshal-append-remarshal pattern for
  796. // cfg.OutboundConfigs. Synthetic rules are prepended ahead of whatever's
  797. // already in the routing rules array, the same pattern injectNodeEgresses/
  798. // injectMtprotoEgress already use for their own always-must-win infra
  799. // rules — this never touches the admin's own saved Routing-page rule
  800. // order.
  801. func injectAmneziawgV6Egress(cfg *xray.Config, inbounds []*model.Inbound) {
  802. // Protocol is checked alongside Tag, not just Tag alone: a tag collision
  803. // with some unrelated (non-socks) inbound must not be mistaken for this
  804. // instance's own relay having been created.
  805. liveInboundTags := make(map[string]struct{}, len(cfg.InboundConfigs))
  806. for i := range cfg.InboundConfigs {
  807. if cfg.InboundConfigs[i].Protocol == "socks" {
  808. liveInboundTags[cfg.InboundConfigs[i].Tag] = struct{}{}
  809. }
  810. }
  811. var existingOutbounds []any
  812. if len(cfg.OutboundConfigs) > 0 {
  813. if err := json.Unmarshal(cfg.OutboundConfigs, &existingOutbounds); err != nil {
  814. logger.Warning("amneziawg v6 egress: outbounds section is unparsable, skipping injection:", err)
  815. return
  816. }
  817. }
  818. usedOutboundTags := make(map[string]struct{}, len(existingOutbounds))
  819. for _, o := range existingOutbounds {
  820. if m, ok := o.(map[string]any); ok {
  821. if t, ok := m["tag"].(string); ok {
  822. usedOutboundTags[t] = struct{}{}
  823. }
  824. }
  825. }
  826. routing := map[string]any{}
  827. if len(cfg.RouterConfig) > 0 {
  828. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  829. logger.Warning("amneziawg v6 egress: routing section is unparsable, skipping injection:", err)
  830. return
  831. }
  832. }
  833. rules, _ := routing["rules"].([]any)
  834. newRules := make([]any, 0)
  835. newOutbounds := make([]any, 0)
  836. for _, inbound := range inbounds {
  837. if inbound.Protocol != model.AmneziaWG || !inbound.Enable || inbound.NodeID != nil {
  838. continue
  839. }
  840. if _, live := liveInboundTags[inbound.Tag]; !live {
  841. // The relay inbound itself wasn't created this pass (e.g. a tag
  842. // collision inside injectAmneziawgnetSocks) -- no SOCKS5 inbound
  843. // exists for hot_diff.go's inboundTag match to ever fire against.
  844. continue
  845. }
  846. inst, ok := amneziawg.InstanceFromInbound(inbound)
  847. if !ok || !amneziawgnet.V6AliasesActive(inst) {
  848. continue
  849. }
  850. for _, p := range inst.Peers {
  851. if p.Email == "" {
  852. continue
  853. }
  854. v6 := amneziawg.FirstIPv6(p.AllowedIPs)
  855. if v6 == "" {
  856. continue
  857. }
  858. tag := amneziawgV6EgressTag(inbound.Id, p.Email)
  859. if _, taken := usedOutboundTags[tag]; taken {
  860. logger.Warning("amneziawg v6 egress: outbound tag [", tag, "] already exists, skipping peer [", p.Email, "]")
  861. continue
  862. }
  863. usedOutboundTags[tag] = struct{}{}
  864. newOutbounds = append(newOutbounds, map[string]any{
  865. "tag": tag,
  866. "protocol": "freedom",
  867. "sendThrough": v6,
  868. "settings": map[string]any{},
  869. })
  870. newRules = append(newRules, map[string]any{
  871. "type": "field",
  872. "inboundTag": []any{inbound.Tag},
  873. "user": []any{p.Email},
  874. "ip": []any{"::/0"},
  875. "outboundTag": tag,
  876. })
  877. }
  878. }
  879. if len(newOutbounds) == 0 {
  880. return
  881. }
  882. merged := make([]any, 0, len(existingOutbounds))
  883. merged = append(merged, existingOutbounds...)
  884. merged = append(merged, newOutbounds...)
  885. combined, err := json.MarshalIndent(merged, "", " ")
  886. if err != nil {
  887. logger.Warning("amneziawg v6 egress: failed to rebuild outbounds section, skipping injection:", err)
  888. return
  889. }
  890. cfg.OutboundConfigs = json_util.RawMessage(combined)
  891. routing["rules"] = append(newRules, rules...)
  892. newRouting, err := json.Marshal(routing)
  893. if err != nil {
  894. logger.Warning("amneziawg v6 egress: failed to rebuild routing section, skipping injection:", err)
  895. return
  896. }
  897. cfg.RouterConfig = json_util.RawMessage(newRouting)
  898. }
  899. // mergeSubscriptionOutbounds appends the subscription outbounds to the
  900. // OutboundConfigs array of the xray config. It works on the already-unmarshaled
  901. // template so that manually configured outbounds are never overwritten.
  902. //
  903. // Safety: if we cannot parse the template's outbounds array, we leave
  904. // OutboundConfigs exactly as it came from the template (we do not inject
  905. // subscription outbounds). This prevents us from accidentally dropping the
  906. // user's manually configured outbounds when the template is in a weird state.
  907. func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
  908. if len(prepend) == 0 && len(appendList) == 0 {
  909. return
  910. }
  911. var templateOutbounds []any
  912. if len(cfg.OutboundConfigs) > 0 {
  913. if err := json.Unmarshal(cfg.OutboundConfigs, &templateOutbounds); err != nil {
  914. // Corrupt template outbounds — do not touch the field at all.
  915. // The user will see problems on Xray start / next save.
  916. return
  917. }
  918. }
  919. var merged []any
  920. merged = append(merged, prepend...)
  921. merged = append(merged, templateOutbounds...)
  922. merged = append(merged, appendList...)
  923. combined, err := json.MarshalIndent(merged, "", " ")
  924. if err != nil {
  925. return
  926. }
  927. cfg.OutboundConfigs = json_util.RawMessage(combined)
  928. }
  929. // ensureAPIServices guarantees the gRPC services the panel depends on are
  930. // listed in the generated config's api block: HandlerService and StatsService
  931. // have always been required for inbound/user management and traffic polling,
  932. // and RoutingService enables hot routing reload on templates saved before it
  933. // was added to the default template. The stored template itself is not
  934. // modified — only the generated runtime config.
  935. func ensureAPIServices(api json_util.RawMessage) json_util.RawMessage {
  936. if len(api) == 0 {
  937. // No api block means the panel's API integration is deliberately
  938. // disabled; don't resurrect it behind the user's back.
  939. return api
  940. }
  941. var parsed map[string]any
  942. if err := json.Unmarshal(api, &parsed); err != nil {
  943. return api
  944. }
  945. services, _ := parsed["services"].([]any)
  946. have := make(map[string]bool, len(services))
  947. for _, svc := range services {
  948. if name, ok := svc.(string); ok {
  949. have[name] = true
  950. }
  951. }
  952. added := false
  953. for _, name := range []string{"HandlerService", "StatsService", "RoutingService"} {
  954. if !have[name] {
  955. services = append(services, name)
  956. added = true
  957. }
  958. }
  959. if !added {
  960. return api
  961. }
  962. parsed["services"] = services
  963. out, err := json.Marshal(parsed)
  964. if err != nil {
  965. return api
  966. }
  967. return out
  968. }
  969. // ensureStatsPolicy guarantees every policy level in the generated config has
  970. // statsUserOnline enabled, so the core tracks per-email online IPs for the
  971. // panel's online view and access-log-free IP limiting. Generated clients carry
  972. // no explicit level, so level "0" is created when absent. The flag is panel
  973. // infrastructure and is forced on even over an explicit false in the template,
  974. // same as the api services above. An entirely missing or unparsable policy
  975. // block is left alone; the stored template itself is never modified — only the
  976. // generated runtime config.
  977. func ensureStatsPolicy(policy json_util.RawMessage) json_util.RawMessage {
  978. if len(policy) == 0 {
  979. return policy
  980. }
  981. var parsed map[string]any
  982. if err := json.Unmarshal(policy, &parsed); err != nil {
  983. return policy
  984. }
  985. levels, _ := parsed["levels"].(map[string]any)
  986. if levels == nil {
  987. levels = make(map[string]any)
  988. }
  989. if _, ok := levels["0"]; !ok {
  990. levels["0"] = map[string]any{}
  991. }
  992. changed := false
  993. for _, raw := range levels {
  994. level, ok := raw.(map[string]any)
  995. if !ok {
  996. continue
  997. }
  998. if enabled, ok := level["statsUserOnline"].(bool); !ok || !enabled {
  999. level["statsUserOnline"] = true
  1000. changed = true
  1001. }
  1002. }
  1003. if !changed {
  1004. return policy
  1005. }
  1006. parsed["levels"] = levels
  1007. out, err := json.Marshal(parsed)
  1008. if err != nil {
  1009. return policy
  1010. }
  1011. return out
  1012. }
  1013. // caseVariantKeys returns every key of parsed that equals want ignoring case,
  1014. // lowest first so the fold is deterministic when several variants are present.
  1015. func caseVariantKeys(parsed map[string]any, want string) []string {
  1016. var keys []string
  1017. for key := range parsed {
  1018. if strings.EqualFold(key, want) {
  1019. keys = append(keys, key)
  1020. }
  1021. }
  1022. slices.Sort(keys)
  1023. return keys
  1024. }
  1025. func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
  1026. if len(logCfg) == 0 {
  1027. return logCfg
  1028. }
  1029. var parsed map[string]any
  1030. if err := json.Unmarshal(logCfg, &parsed); err != nil {
  1031. return logCfg
  1032. }
  1033. changed := false
  1034. for _, key := range []string{"access", "error"} {
  1035. // xray-core decodes this object with encoding/json, whose case-insensitive
  1036. // field match makes "Access" reach AccessLog too — fold every variant.
  1037. variants := caseVariantKeys(parsed, key)
  1038. value, hasValue := parsed[key]
  1039. for _, variant := range variants {
  1040. if variant == key {
  1041. continue
  1042. }
  1043. if !hasValue {
  1044. value, hasValue = parsed[variant], true
  1045. }
  1046. delete(parsed, variant)
  1047. changed = true
  1048. }
  1049. v, ok := value.(string)
  1050. if !ok {
  1051. continue
  1052. }
  1053. trimmed := strings.TrimSpace(v)
  1054. if trimmed == "" || strings.EqualFold(trimmed, "none") {
  1055. if changed {
  1056. parsed[key] = v
  1057. }
  1058. continue
  1059. }
  1060. base := path.Base(filepath.ToSlash(trimmed))
  1061. if base == "" || base == "." || base == ".." || base == "/" {
  1062. continue
  1063. }
  1064. confined := filepath.Join(config.GetLogFolder(), base)
  1065. if confined == trimmed {
  1066. continue
  1067. }
  1068. parsed[key] = confined
  1069. changed = true
  1070. }
  1071. if !changed {
  1072. return logCfg
  1073. }
  1074. out, err := json.Marshal(parsed)
  1075. if err != nil {
  1076. return logCfg
  1077. }
  1078. return out
  1079. }
  1080. // stripDisabledRules removes routing rules marked `enabled: false` from the
  1081. // generated runtime config and strips panel-only keys (`enabled`, `comment`)
  1082. // from the rest, since xray-core has no such fields. The internal api rule is
  1083. // always kept (see isApiRule) so traffic stats can't be toggled off. The stored
  1084. // template is untouched — only the generated config is filtered.
  1085. func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
  1086. if len(routerCfg) == 0 {
  1087. return routerCfg
  1088. }
  1089. var parsed map[string]any
  1090. if err := json.Unmarshal(routerCfg, &parsed); err != nil {
  1091. return routerCfg
  1092. }
  1093. rules, ok := parsed["rules"].([]any)
  1094. if !ok || len(rules) == 0 {
  1095. return routerCfg
  1096. }
  1097. var activeRules []any
  1098. changed := false
  1099. for _, rawRule := range rules {
  1100. rule, ok := rawRule.(map[string]any)
  1101. if !ok {
  1102. activeRules = append(activeRules, rawRule)
  1103. continue
  1104. }
  1105. if enabledRaw, exists := rule["enabled"]; exists {
  1106. // The internal api rule carries traffic stats and must never be
  1107. // dropped, even if it was somehow marked disabled.
  1108. enabled, ok := enabledRaw.(bool)
  1109. if ok && !enabled && !isApiRule(rule) {
  1110. changed = true
  1111. continue
  1112. }
  1113. delete(rule, "enabled")
  1114. changed = true
  1115. }
  1116. if _, exists := rule["comment"]; exists {
  1117. delete(rule, "comment")
  1118. changed = true
  1119. }
  1120. activeRules = append(activeRules, rule)
  1121. }
  1122. if !changed {
  1123. return routerCfg
  1124. }
  1125. parsed["rules"] = activeRules
  1126. out, err := json.Marshal(parsed)
  1127. if err != nil {
  1128. return routerCfg
  1129. }
  1130. return out
  1131. }
  1132. // GetXrayTraffic fetches the current traffic statistics from the running Xray process.
  1133. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
  1134. process := currentXrayProcess()
  1135. if process == nil || !process.IsRunning() {
  1136. err := errors.New("xray is not running")
  1137. logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
  1138. return nil, nil, err
  1139. }
  1140. apiPort := process.GetAPIPort()
  1141. if err := s.xrayAPI.Init(apiPort); err != nil {
  1142. logger.Debug("Failed to initialize Xray API:", err)
  1143. return nil, nil, err
  1144. }
  1145. defer s.xrayAPI.Close()
  1146. traffic, clientTraffic, err := s.xrayAPI.GetTraffic()
  1147. if err != nil {
  1148. logger.Debug("Failed to fetch Xray traffic:", err)
  1149. return nil, nil, err
  1150. }
  1151. return traffic, clientTraffic, nil
  1152. }
  1153. // GetOnlineUsers returns connection-based online users (email + source IPs)
  1154. // from the running core's online-stats API. ok=false means the API is not
  1155. // available — xray isn't running or the core predates the online-stats RPCs —
  1156. // and callers must use the legacy traffic-delta / access-log paths. The
  1157. // capability is probed lazily per process: an Unimplemented answer pins this
  1158. // core as unsupported until the next restart, while transient errors leave the
  1159. // capability undecided so a flaky poll can't lock in legacy mode.
  1160. func (s *XrayService) GetOnlineUsers() ([]xray.OnlineUser, bool, error) {
  1161. process := currentXrayProcess()
  1162. if process == nil || !process.IsRunning() {
  1163. return nil, false, nil
  1164. }
  1165. if process.OnlineAPISupport() == xray.OnlineAPIUnsupported {
  1166. return nil, false, nil
  1167. }
  1168. if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
  1169. logger.Debug("Failed to initialize Xray API:", err)
  1170. return nil, false, err
  1171. }
  1172. defer s.xrayAPI.Close()
  1173. users, err := s.xrayAPI.GetOnlineUsers()
  1174. if err != nil {
  1175. if xray.IsUnimplementedErr(err) {
  1176. process.SetOnlineAPISupport(xray.OnlineAPIUnsupported)
  1177. logger.Info("xray core does not support the online-stats API; falling back to traffic-delta onlines and access-log IP limit")
  1178. return nil, false, nil
  1179. }
  1180. logger.Debug("Failed to fetch Xray online users:", err)
  1181. return nil, false, err
  1182. }
  1183. if process.OnlineAPISupport() == xray.OnlineAPIUnknown {
  1184. process.SetOnlineAPISupport(xray.OnlineAPISupported)
  1185. logger.Info("xray core supports the online-stats API; using connection-based onlines and access-log-free IP limit")
  1186. }
  1187. return users, true, nil
  1188. }
  1189. // BalancerStatus is the live view of one balancer for the panel UI. Running
  1190. // is false when the balancer isn't present in the running core (e.g. xray is
  1191. // stopped or the balancer hasn't been saved/applied yet).
  1192. type BalancerStatus struct {
  1193. Tag string `json:"tag"`
  1194. Running bool `json:"running"`
  1195. Override string `json:"override"`
  1196. Selected []string `json:"selected"`
  1197. }
  1198. // GetBalancersStatus queries the running core for the live state of the
  1199. // given balancer tags. Per-tag failures are reported as Running=false rather
  1200. // than failing the whole call, so the UI can render saved-but-not-applied
  1201. // balancers alongside live ones.
  1202. func (s *XrayService) GetBalancersStatus(tags []string) ([]BalancerStatus, error) {
  1203. statuses := make([]BalancerStatus, 0, len(tags))
  1204. process := currentXrayProcess()
  1205. if process == nil || !process.IsRunning() {
  1206. for _, tag := range tags {
  1207. statuses = append(statuses, BalancerStatus{Tag: tag})
  1208. }
  1209. return statuses, nil
  1210. }
  1211. if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
  1212. return nil, err
  1213. }
  1214. defer s.xrayAPI.Close()
  1215. for _, tag := range tags {
  1216. info, err := s.xrayAPI.GetBalancerInfo(tag)
  1217. if err != nil {
  1218. logger.Debug("get balancer info [", tag, "] failed:", err)
  1219. statuses = append(statuses, BalancerStatus{Tag: tag})
  1220. continue
  1221. }
  1222. statuses = append(statuses, BalancerStatus{
  1223. Tag: tag,
  1224. Running: true,
  1225. Override: info.Override,
  1226. Selected: info.Selected,
  1227. })
  1228. }
  1229. return statuses, nil
  1230. }
  1231. // OverrideBalancer forces a balancer in the running core to use the given
  1232. // outbound tag; an empty target clears the override. When target names
  1233. // another balancer, the override resolves to the loopback outbound that
  1234. // routes traffic through the target balancer via the routing rules.
  1235. func (s *XrayService) OverrideBalancer(tag, target string) error {
  1236. process := currentXrayProcess()
  1237. if process == nil || !process.IsRunning() {
  1238. return errors.New("xray is not running")
  1239. }
  1240. if target != "" {
  1241. resolved, err := s.resolveOverrideTarget(target)
  1242. if err != nil {
  1243. return err
  1244. }
  1245. if resolved != "" {
  1246. target = resolved
  1247. }
  1248. }
  1249. if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
  1250. return err
  1251. }
  1252. defer s.xrayAPI.Close()
  1253. return s.xrayAPI.SetBalancerTarget(tag, target)
  1254. }
  1255. // resolveOverrideTarget checks if target names a balancer and, if so,
  1256. // returns the loopback outbound tag that routes to it through the
  1257. // routing rules. Returns empty if target is already a concrete outbound.
  1258. func (s *XrayService) resolveOverrideTarget(target string) (string, error) {
  1259. template, err := s.settingService.GetXrayConfigTemplate()
  1260. if err != nil {
  1261. return "", err
  1262. }
  1263. var cfg map[string]any
  1264. if err := json.Unmarshal([]byte(template), &cfg); err != nil {
  1265. return "", err
  1266. }
  1267. routing, _ := cfg["routing"].(map[string]any)
  1268. if routing == nil {
  1269. return "", nil
  1270. }
  1271. rules, _ := routing["rules"].([]any)
  1272. for _, r := range rules {
  1273. rule, ok := r.(map[string]any)
  1274. if !ok {
  1275. continue
  1276. }
  1277. if rule["balancerTag"] != target {
  1278. continue
  1279. }
  1280. inboundTags, ok := rule["inboundTag"].([]any)
  1281. if !ok || len(inboundTags) == 0 {
  1282. continue
  1283. }
  1284. if lbTag, ok := inboundTags[0].(string); ok && strings.HasPrefix(lbTag, "_bl_") {
  1285. return lbTag, nil
  1286. }
  1287. }
  1288. return "", nil
  1289. }
  1290. // TestRoute asks the running core which outbound its router picks for the
  1291. // described connection.
  1292. func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
  1293. process := currentXrayProcess()
  1294. if process == nil || !process.IsRunning() {
  1295. return nil, errors.New("xray is not running")
  1296. }
  1297. if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
  1298. return nil, err
  1299. }
  1300. defer s.xrayAPI.Close()
  1301. return s.xrayAPI.TestRoute(req)
  1302. }
  1303. // RestartXray reconciles the running Xray process with the current desired
  1304. // config. When isForce is false it first tries to apply the changes through
  1305. // the Xray gRPC API without restarting the process (inbounds, outbounds and
  1306. // routing rules/balancers are hot-reloadable); only changes the core cannot
  1307. // take at runtime — or a force request — stop and restart the process.
  1308. func (s *XrayService) RestartXray(isForce bool) error {
  1309. lock.Lock()
  1310. defer lock.Unlock()
  1311. logger.Debug("restart Xray, force:", isForce)
  1312. if !isForce && isManuallyStopped.Load() {
  1313. return nil
  1314. }
  1315. isManuallyStopped.Store(false)
  1316. xrayConfig, err := s.GetXrayConfig()
  1317. if err != nil {
  1318. return err
  1319. }
  1320. process := currentXrayProcess()
  1321. if process != nil && process.IsRunning() {
  1322. configUnchanged := process.GetConfig().Equals(xrayConfig)
  1323. if !isForce && configUnchanged && !isNeedXrayRestart.Load() {
  1324. logger.Debug("It does not need to restart Xray")
  1325. return nil
  1326. }
  1327. // A config the core cannot bind never replaces one that works: its failed
  1328. // start exits the core, and the watchdog would then loop on it forever.
  1329. if conflicts := bindConflicts(xrayConfig, process.GetConfig()); len(conflicts) > 0 {
  1330. refused := fmt.Sprintf("config refused: %s", conflicts[0])
  1331. for _, conflict := range conflicts {
  1332. logger.Error("xray config refused:", conflict.String())
  1333. }
  1334. // The refusal is otherwise invisible: the operator's request
  1335. // succeeded, so the status page has to carry the stale state.
  1336. xrayState.holdBack(refused)
  1337. return fmt.Errorf("xray %s", refused)
  1338. }
  1339. if !isForce && !configUnchanged && s.tryHotApply(process, xrayConfig) {
  1340. logger.Info("Xray config changes applied through the core API, no restart needed")
  1341. return nil
  1342. }
  1343. _ = process.Stop()
  1344. } else if conflicts := bindConflicts(xrayConfig, nil); len(conflicts) > 0 {
  1345. // Nothing is running to protect and the core is the authority on what it
  1346. // can bind: start it and let its own error name the port it lost.
  1347. logger.Warning("xray config may not start:", conflicts[0].String())
  1348. }
  1349. process = xray.NewProcess(xrayConfig)
  1350. xrayState.replace(process)
  1351. s.xrayAPI.StatsLastValues = nil
  1352. err = process.Start()
  1353. if err != nil {
  1354. return err
  1355. }
  1356. return nil
  1357. }
  1358. // restartToDropClients reports whether a diff that strands clients must be
  1359. // applied by restarting instead of through the API.
  1360. func (s *XrayService) restartToDropClients(diff *xray.HotDiff) bool {
  1361. if diff == nil || !diff.DropsUsers() {
  1362. return false
  1363. }
  1364. restart, err := s.settingService.GetRestartXrayOnClientDisable()
  1365. if err != nil {
  1366. logger.Warning("get RestartXrayOnClientDisable failed:", err)
  1367. return false
  1368. }
  1369. return restart
  1370. }
  1371. // tryHotApply attempts to reconcile the running Xray instance with newCfg
  1372. // through the core gRPC API (HandlerService for inbounds/outbounds,
  1373. // RoutingService for rules/balancers). It returns true when the running
  1374. // instance now matches newCfg; on any failure it returns false and the
  1375. // caller falls back to a full process restart, which cleans up whatever was
  1376. // partially applied. Callers must hold the package-level lock.
  1377. func (s *XrayService) tryHotApply(process *xray.Process, newCfg *xray.Config) bool {
  1378. oldCfg := process.GetConfig()
  1379. diff, ok := xray.ComputeHotDiff(oldCfg, newCfg)
  1380. if !ok {
  1381. logger.Debug("hot apply: config change is not API-applicable, falling back to restart")
  1382. return false
  1383. }
  1384. if diff.Empty() {
  1385. process.SetConfig(newCfg)
  1386. persistHotConfig(process)
  1387. return true
  1388. }
  1389. // The core's RemoveUser drops the credential only, so a disabled or deleted
  1390. // client needs the restart this setting asks for.
  1391. if s.restartToDropClients(diff) {
  1392. logger.Info("hot apply: clients left the config, restarting to drop their live sessions")
  1393. return false
  1394. }
  1395. apiPort := process.GetAPIPort()
  1396. if apiPort <= 0 {
  1397. return false
  1398. }
  1399. // A dedicated client: s.xrayAPI may be in use by traffic polling on other
  1400. // service instances and is reset around restarts.
  1401. hotAPI := xray.XrayAPI{}
  1402. if err := hotAPI.Init(apiPort); err != nil {
  1403. logger.Debug("hot apply: failed to init xray api:", err)
  1404. return false
  1405. }
  1406. defer hotAPI.Close()
  1407. // Removals first so changed handlers and port swaps never collide with
  1408. // the additions that follow.
  1409. for _, u := range diff.RemovedUsers {
  1410. if err := hotAPI.RemoveUser(u.Tag, u.Email); err != nil && !xray.IsMissingHandlerErr(err) {
  1411. logger.Info("hot apply: remove user [", u.Email, "] from [", u.Tag, "] failed:", err)
  1412. return false
  1413. }
  1414. }
  1415. for _, tag := range diff.RemovedInboundTags {
  1416. if err := hotAPI.DelInbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  1417. logger.Info("hot apply: remove inbound [", tag, "] failed:", err)
  1418. return false
  1419. }
  1420. }
  1421. for _, tag := range diff.RemovedOutboundTags {
  1422. if err := hotAPI.DelOutbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  1423. logger.Info("hot apply: remove outbound [", tag, "] failed:", err)
  1424. return false
  1425. }
  1426. }
  1427. for _, ob := range diff.AddedOutbounds {
  1428. if err := addOutboundReconciling(&hotAPI, ob); err != nil {
  1429. logger.Info("hot apply: add outbound failed:", err)
  1430. return false
  1431. }
  1432. }
  1433. for _, ib := range diff.AddedInbounds {
  1434. if err := addInboundReconciling(&hotAPI, ib); err != nil {
  1435. logger.Info("hot apply: add inbound failed:", err)
  1436. return false
  1437. }
  1438. }
  1439. for _, u := range diff.AddedUsers {
  1440. if err := addUserReconciling(&hotAPI, u); err != nil {
  1441. logger.Info("hot apply: add user [", u.Email, "] to [", u.Tag, "] failed:", err)
  1442. return false
  1443. }
  1444. }
  1445. if diff.RoutingConfig != nil {
  1446. if err := hotAPI.ApplyRoutingConfig(diff.RoutingConfig); err != nil {
  1447. logger.Info("hot apply: apply routing config failed:", err)
  1448. return false
  1449. }
  1450. }
  1451. process.SetConfig(newCfg)
  1452. persistHotConfig(process)
  1453. return true
  1454. }
  1455. // persistHotConfig refreshes config.json after a hot apply; a write failure is
  1456. // logged only, since the running core already has the change.
  1457. func persistHotConfig(process *xray.Process) {
  1458. if err := process.PersistConfig(); err != nil {
  1459. logger.Warning("hot apply: failed to update config.json:", err)
  1460. }
  1461. }
  1462. // addUserReconciling adds a user, and on an email conflict (the user was
  1463. // already applied through the runtime API) replaces the existing user instead.
  1464. func addUserReconciling(api *xray.XrayAPI, u xray.UserOp) error {
  1465. err := api.AddUser(u.Protocol, u.Tag, u.User)
  1466. if err == nil || !xray.IsUserExistsErr(err) {
  1467. return err
  1468. }
  1469. if delErr := api.RemoveUser(u.Tag, u.Email); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1470. return delErr
  1471. }
  1472. return api.AddUser(u.Protocol, u.Tag, u.User)
  1473. }
  1474. // addInboundReconciling adds an inbound, and on a tag conflict (the handler
  1475. // was already created through the runtime API while the stored snapshot was
  1476. // stale) replaces the existing handler instead.
  1477. func addInboundReconciling(api *xray.XrayAPI, inbound []byte) error {
  1478. err := api.AddInbound(inbound)
  1479. if err == nil || !xray.IsExistingTagErr(err) {
  1480. return err
  1481. }
  1482. var meta struct {
  1483. Tag string `json:"tag"`
  1484. }
  1485. if jsonErr := json.Unmarshal(inbound, &meta); jsonErr != nil || meta.Tag == "" {
  1486. return err
  1487. }
  1488. if delErr := api.DelInbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1489. return delErr
  1490. }
  1491. return api.AddInbound(inbound)
  1492. }
  1493. // addOutboundReconciling mirrors addInboundReconciling for outbounds.
  1494. func addOutboundReconciling(api *xray.XrayAPI, outbound []byte) error {
  1495. err := api.AddOutbound(outbound)
  1496. if err == nil || !xray.IsExistingTagErr(err) {
  1497. return err
  1498. }
  1499. var meta struct {
  1500. Tag string `json:"tag"`
  1501. }
  1502. if jsonErr := json.Unmarshal(outbound, &meta); jsonErr != nil || meta.Tag == "" {
  1503. return err
  1504. }
  1505. if delErr := api.DelOutbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  1506. return delErr
  1507. }
  1508. return api.AddOutbound(outbound)
  1509. }
  1510. // StopXray stops the running Xray process.
  1511. func (s *XrayService) StopXray() error {
  1512. lock.Lock()
  1513. defer lock.Unlock()
  1514. isManuallyStopped.Store(true)
  1515. logger.Debug("Attempting to stop Xray...")
  1516. process := currentXrayProcess()
  1517. if process != nil && process.IsRunning() {
  1518. return process.Stop()
  1519. }
  1520. return errors.New("xray is not running")
  1521. }
  1522. // SetToNeedRestart marks that Xray needs to be restarted.
  1523. func (s *XrayService) SetToNeedRestart() {
  1524. isNeedXrayRestart.Store(true)
  1525. }
  1526. // GetXrayAPIPort returns the port the local xray process is listening on
  1527. // for its gRPC HandlerService, or 0 when xray isn't currently running.
  1528. // Exposed for the runtime package's LocalRuntime adapter without a
  1529. // service-package import cycle.
  1530. func (s *XrayService) GetXrayAPIPort() int {
  1531. process := currentXrayProcess()
  1532. if process == nil || !process.IsRunning() {
  1533. return 0
  1534. }
  1535. return process.GetAPIPort()
  1536. }
  1537. // IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
  1538. func (s *XrayService) IsNeedRestartAndSetFalse() bool {
  1539. return isNeedXrayRestart.CompareAndSwap(true, false)
  1540. }
  1541. // ApplyPendingRestart consumes the need-restart flag and restarts Xray. If the
  1542. // restart fails (for example GetXrayConfig hits a transient DB error and leaves
  1543. // the old process running), it re-arms the flag so the next tick retries instead
  1544. // of silently dropping the pending config change.
  1545. func (s *XrayService) ApplyPendingRestart() {
  1546. if !s.IsNeedRestartAndSetFalse() {
  1547. return
  1548. }
  1549. if err := s.RestartXray(false); err != nil {
  1550. logger.Error("restart xray failed:", err)
  1551. s.SetToNeedRestart()
  1552. }
  1553. }
  1554. // DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
  1555. func (s *XrayService) DidXrayCrash() bool {
  1556. return !s.IsXrayRunning() && !isManuallyStopped.Load()
  1557. }
  1558. // liftXhttpSessionIDKeys renames the legacy XHTTP session keys
  1559. // (sessionPlacement/sessionKey) to the v26.6.22 #6258 names
  1560. // (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept
  1561. // no fallback for the old names, so a config stored before the rename would be
  1562. // silently ignored by the engine. Returns true if it changed anything.
  1563. func liftXhttpSessionIDKeys(stream map[string]any) bool {
  1564. xhttp, ok := stream["xhttpSettings"].(map[string]any)
  1565. if !ok {
  1566. return false
  1567. }
  1568. changed := false
  1569. for legacy, renamed := range map[string]string{
  1570. "sessionPlacement": "sessionIDPlacement",
  1571. "sessionKey": "sessionIDKey",
  1572. } {
  1573. v, has := xhttp[legacy]
  1574. if !has {
  1575. continue
  1576. }
  1577. if _, exists := xhttp[renamed]; !exists {
  1578. xhttp[renamed] = v
  1579. }
  1580. delete(xhttp, legacy)
  1581. changed = true
  1582. }
  1583. return changed
  1584. }
  1585. // healOutboundStreams applies every heal to each outbound's streamSettings, returning the
  1586. // original bytes when none changed anything so the hot-reload diff sees no edit.
  1587. func healOutboundStreams(raw json_util.RawMessage, heals ...func(stream map[string]any) bool) json_util.RawMessage {
  1588. if len(raw) == 0 {
  1589. return raw
  1590. }
  1591. var outbounds []map[string]any
  1592. if err := json.Unmarshal(raw, &outbounds); err != nil {
  1593. return raw
  1594. }
  1595. changed := false
  1596. for _, ob := range outbounds {
  1597. if stream, ok := ob["streamSettings"].(map[string]any); ok {
  1598. for _, heal := range heals {
  1599. if heal(stream) {
  1600. changed = true
  1601. }
  1602. }
  1603. }
  1604. }
  1605. if !changed {
  1606. return raw
  1607. }
  1608. if rewritten, err := json.Marshal(outbounds); err == nil {
  1609. return rewritten
  1610. }
  1611. return raw
  1612. }
  1613. func upgradeStreamLegacyXdns(stream map[string]any) bool {
  1614. return maskcompat.UpgradeLegacyXdns(stream["finalmask"])
  1615. }
  1616. func upgradeStreamLegacyUDPHop(stream map[string]any) bool {
  1617. return maskcompat.UpgradeLegacyUDPHop(stream["finalmask"])
  1618. }
  1619. func normalizeStreamUDPHop(stream map[string]any) bool {
  1620. return maskcompat.NormalizeUDPHop(stream["finalmask"])
  1621. }