xray.go 52 KB

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