xray.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "path"
  6. "path/filepath"
  7. "runtime"
  8. "strings"
  9. "sync"
  10. "github.com/mhsanaei/3x-ui/v3/internal/config"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  12. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  13. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  14. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  15. "go.uber.org/atomic"
  16. )
  17. var (
  18. p *xray.Process
  19. lock sync.Mutex
  20. isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
  21. isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
  22. result string
  23. )
  24. // XrayService provides business logic for Xray process management.
  25. // It handles starting, stopping, restarting Xray, and managing its configuration.
  26. type XrayService struct {
  27. inboundService InboundService
  28. settingService SettingService
  29. xrayAPI xray.XrayAPI
  30. }
  31. // IsXrayRunning checks if the Xray process is currently running.
  32. func (s *XrayService) IsXrayRunning() bool {
  33. return p != nil && p.IsRunning()
  34. }
  35. // XrayProcess returns the current Xray process instance (may be nil when Xray
  36. // is not running). It exposes the package-level process to callers outside this
  37. // package (e.g. the tgbot subpackage) without changing access semantics.
  38. func XrayProcess() *xray.Process {
  39. return p
  40. }
  41. // GetXrayErr returns the error from the Xray process, if any.
  42. func (s *XrayService) GetXrayErr() error {
  43. if p == nil {
  44. return nil
  45. }
  46. err := p.GetErr()
  47. if err == nil {
  48. return nil
  49. }
  50. if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
  51. // exit status 1 on Windows means that Xray process was killed
  52. // as we kill process to stop in on Windows, this is not an error
  53. return nil
  54. }
  55. return err
  56. }
  57. // GetXrayResult returns the result string from the Xray process.
  58. func (s *XrayService) GetXrayResult() string {
  59. if result != "" {
  60. return result
  61. }
  62. if s.IsXrayRunning() {
  63. return ""
  64. }
  65. if p == nil {
  66. return ""
  67. }
  68. result = p.GetResult()
  69. if runtime.GOOS == "windows" && result == "exit status 1" {
  70. // exit status 1 on Windows means that Xray process was killed
  71. // as we kill process to stop in on Windows, this is not an error
  72. return ""
  73. }
  74. return result
  75. }
  76. // GetXrayVersion returns the version of the running Xray process.
  77. func (s *XrayService) GetXrayVersion() string {
  78. if p == nil {
  79. return "Unknown"
  80. }
  81. return p.GetVersion()
  82. }
  83. // RemoveIndex removes an element at the specified index from a slice.
  84. // Returns a new slice with the element removed.
  85. func RemoveIndex(s []any, index int) []any {
  86. return append(s[:index], s[index+1:]...)
  87. }
  88. // GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
  89. func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
  90. templateConfig, err := s.settingService.GetXrayConfigTemplate()
  91. if err != nil {
  92. return nil, err
  93. }
  94. xrayConfig := &xray.Config{}
  95. err = json.Unmarshal([]byte(templateConfig), xrayConfig)
  96. if err != nil {
  97. return nil, err
  98. }
  99. xrayConfig.LogConfig = resolveXrayLogPaths(xrayConfig.LogConfig)
  100. xrayConfig.API = ensureAPIServices(xrayConfig.API)
  101. xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
  102. _, _, _ = s.inboundService.AddTraffic(nil, nil)
  103. inbounds, err := s.inboundService.GetAllInbounds()
  104. if err != nil {
  105. return nil, err
  106. }
  107. for _, inbound := range inbounds {
  108. if !inbound.Enable {
  109. continue
  110. }
  111. if inbound.NodeID != nil {
  112. continue
  113. }
  114. if inbound.Protocol == model.MTProto {
  115. continue
  116. }
  117. settings := map[string]any{}
  118. json.Unmarshal([]byte(inbound.Settings), &settings)
  119. dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id)
  120. if listErr != nil {
  121. return nil, listErr
  122. }
  123. clientStats := inbound.ClientStats
  124. enableMap := make(map[string]bool, len(clientStats))
  125. for _, clientTraffic := range clientStats {
  126. enableMap[clientTraffic.Email] = clientTraffic.Enable
  127. }
  128. var finalClients []any
  129. for i := range dbClients {
  130. c := dbClients[i]
  131. if enable, exists := enableMap[c.Email]; exists && !enable {
  132. logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c.Email)
  133. continue
  134. }
  135. if !c.Enable {
  136. continue
  137. }
  138. flow := c.Flow
  139. if flow == "xtls-rprx-vision-udp443" {
  140. flow = "xtls-rprx-vision"
  141. }
  142. entry := map[string]any{"email": c.Email}
  143. switch inbound.Protocol {
  144. case model.VLESS:
  145. if c.ID != "" {
  146. entry["id"] = c.ID
  147. }
  148. if flow != "" {
  149. entry["flow"] = flow
  150. }
  151. if c.Reverse != nil {
  152. entry["reverse"] = c.Reverse
  153. }
  154. case model.VMESS:
  155. if c.ID != "" {
  156. entry["id"] = c.ID
  157. }
  158. if c.Security != "" {
  159. entry["security"] = c.Security
  160. }
  161. case model.Trojan:
  162. if c.Password != "" {
  163. entry["password"] = c.Password
  164. }
  165. if flow != "" {
  166. entry["flow"] = flow
  167. }
  168. case model.Shadowsocks:
  169. if c.Password != "" {
  170. entry["password"] = c.Password
  171. }
  172. case model.Hysteria:
  173. if c.Auth != "" {
  174. entry["auth"] = c.Auth
  175. }
  176. }
  177. finalClients = append(finalClients, entry)
  178. }
  179. _, hadClients := settings["clients"]
  180. mutated := hadClients || len(finalClients) > 0
  181. if mutated {
  182. settings["clients"] = finalClients
  183. }
  184. if inboundCanHostFallbacks(inbound) {
  185. fallbacks, fbErr := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id)
  186. if fbErr != nil {
  187. return nil, fbErr
  188. }
  189. if len(fallbacks) > 0 {
  190. generic := make([]any, 0, len(fallbacks))
  191. for _, f := range fallbacks {
  192. generic = append(generic, f)
  193. }
  194. settings["fallbacks"] = generic
  195. mutated = true
  196. }
  197. }
  198. if mutated {
  199. modifiedSettings, err := json.MarshalIndent(settings, "", " ")
  200. if err != nil {
  201. return nil, err
  202. }
  203. inbound.Settings = string(modifiedSettings)
  204. }
  205. if len(inbound.StreamSettings) > 0 {
  206. // Unmarshal stream JSON
  207. var stream map[string]any
  208. json.Unmarshal([]byte(inbound.StreamSettings), &stream)
  209. // Remove the "settings" field under "tlsSettings" and "realitySettings"
  210. tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
  211. realitySettings, ok2 := stream["realitySettings"].(map[string]any)
  212. if ok1 || ok2 {
  213. if ok1 {
  214. delete(tlsSettings, "settings")
  215. } else if ok2 {
  216. delete(realitySettings, "settings")
  217. }
  218. }
  219. delete(stream, "externalProxy")
  220. newStream, err := json.MarshalIndent(stream, "", " ")
  221. if err != nil {
  222. return nil, err
  223. }
  224. inbound.StreamSettings = string(newStream)
  225. }
  226. if inbound.Protocol == model.Shadowsocks {
  227. if healed, ok := model.HealShadowsocksClientMethods(inbound.Settings); ok {
  228. inbound.Settings = healed
  229. }
  230. }
  231. inboundConfig := inbound.GenXrayInboundConfig()
  232. xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
  233. }
  234. // Merge subscription-derived outbounds (if any) into the final outbounds array.
  235. // These are additive: each subscription is placed before or after the template
  236. // outbounds based on its Prepend flag, ordered by Priority. Tags assigned by the
  237. // subscription service are kept stable across refreshes so that balancers and
  238. // routing rules continue to work.
  239. subSvc := &OutboundSubscriptionService{}
  240. if prepend, appendList, err := subSvc.activeOutboundsSplit(); err == nil && (len(prepend) > 0 || len(appendList) > 0) {
  241. mergeSubscriptionOutbounds(xrayConfig, prepend, appendList)
  242. }
  243. // Route opted-in local mtproto inbounds through the core's router. Each one
  244. // gets a loopback SOCKS bridge — tagged with the inbound's own tag so it is
  245. // matchable in routing rules — that its mtg sidecar dials Telegram through.
  246. // Done after the subscription merge so a selected subscription outbound (or
  247. // balancer) is a valid rule target.
  248. for i := range inbounds {
  249. inbound := inbounds[i]
  250. if inbound.Protocol != model.MTProto || !inbound.Enable || inbound.NodeID != nil {
  251. continue
  252. }
  253. injectMtprotoEgress(xrayConfig, inbound)
  254. }
  255. // Wire the panel's own HTTP traffic through the configured outbound, after
  256. // the subscription merge so subscription outbound tags are valid targets.
  257. if egressTag, err := s.settingService.GetPanelOutbound(); err != nil {
  258. logger.Warning("read panelOutbound setting failed:", err)
  259. } else if egressTag != "" {
  260. injectPanelEgress(xrayConfig, egressTag)
  261. }
  262. return xrayConfig, nil
  263. }
  264. // PanelEgressInboundTag is the tag of the loopback SOCKS inbound injected into
  265. // the generated config when a panel outbound is configured. The panel's own
  266. // HTTP clients dial through it to egress via the chosen outbound.
  267. const PanelEgressInboundTag = "panel-egress"
  268. // panelEgressBasePort is the first port tried for the egress bridge; ports
  269. // already taken by other inbounds in the generated config are skipped.
  270. const panelEgressBasePort = 62790
  271. // injectPanelEgress appends a loopback SOCKS inbound to the generated config
  272. // and prepends a routing rule sending it to outboundTag. Both live only in the
  273. // generated config — the stored template is never modified — and both are
  274. // hot-appliable, so changing the panel outbound never restarts the core.
  275. func injectPanelEgress(cfg *xray.Config, outboundTag string) {
  276. for i := range cfg.InboundConfigs {
  277. if cfg.InboundConfigs[i].Tag == PanelEgressInboundTag {
  278. logger.Warning("panel egress: inbound tag [", PanelEgressInboundTag, "] already exists, skipping injection")
  279. return
  280. }
  281. }
  282. // The rule must exist before the inbound takes traffic, otherwise the
  283. // bridge would silently egress through the default outbound instead.
  284. routing := map[string]any{}
  285. if len(cfg.RouterConfig) > 0 {
  286. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  287. logger.Warning("panel egress: routing section is unparsable, skipping injection:", err)
  288. return
  289. }
  290. }
  291. rules, _ := routing["rules"].([]any)
  292. rule := map[string]any{
  293. "type": "field",
  294. "inboundTag": []any{PanelEgressInboundTag},
  295. }
  296. // The configured tag may name a routing balancer instead of a concrete
  297. // outbound. A field rule can target either, so emit the matching key —
  298. // balancerTag load-balances the panel's own traffic across the balancer's
  299. // outbounds, while a plain outbound tag keeps the original behavior.
  300. if routingTagIsBalancer(routing, outboundTag) {
  301. rule["balancerTag"] = outboundTag
  302. } else {
  303. rule["outboundTag"] = outboundTag
  304. }
  305. routing["rules"] = append([]any{rule}, rules...)
  306. newRouting, err := json.Marshal(routing)
  307. if err != nil {
  308. logger.Warning("panel egress: failed to rebuild routing section, skipping injection:", err)
  309. return
  310. }
  311. cfg.RouterConfig = json_util.RawMessage(newRouting)
  312. used := make(map[int]struct{}, len(cfg.InboundConfigs))
  313. for i := range cfg.InboundConfigs {
  314. used[cfg.InboundConfigs[i].Port] = struct{}{}
  315. }
  316. port := panelEgressBasePort
  317. for {
  318. if _, taken := used[port]; !taken {
  319. break
  320. }
  321. port++
  322. }
  323. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  324. Listen: json_util.RawMessage(`"127.0.0.1"`),
  325. Port: port,
  326. Protocol: "socks",
  327. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  328. Tag: PanelEgressInboundTag,
  329. })
  330. }
  331. // routingTagIsBalancer reports whether tag names a balancer in the parsed
  332. // routing section. The panel-egress rule targets a balancer via balancerTag and
  333. // a concrete outbound via outboundTag, so the caller picks the key from this.
  334. func routingTagIsBalancer(routing map[string]any, tag string) bool {
  335. if tag == "" {
  336. return false
  337. }
  338. balancers, ok := routing["balancers"].([]any)
  339. if !ok {
  340. return false
  341. }
  342. for _, b := range balancers {
  343. bm, ok := b.(map[string]any)
  344. if !ok {
  345. continue
  346. }
  347. if t, ok := bm["tag"].(string); ok && t == tag {
  348. return true
  349. }
  350. }
  351. return false
  352. }
  353. // mtprotoEgressSocksSettings is the loopback SOCKS server a routed mtproto
  354. // inbound exposes for its mtg sidecar to dial Telegram through. mtg makes plain
  355. // TCP connections, so UDP is left off (matching the panel egress bridge).
  356. const mtprotoEgressSocksSettings = `{"auth":"noauth","udp":false}`
  357. // injectMtprotoEgress wires one routed mtproto inbound into the generated
  358. // config: it appends a loopback SOCKS inbound (tagged with the inbound's own tag,
  359. // on the egress port persisted in settings) and, when an outbound is selected,
  360. // prepends a routing rule sending that tag to it. Both live only in the generated
  361. // config — the stored template is untouched — and both are hot-appliable, so
  362. // toggling routing never forces a full Xray restart. Mirrors injectPanelEgress.
  363. func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
  364. var parsed struct {
  365. RouteThroughXray bool `json:"routeThroughXray"`
  366. RouteXrayPort int `json:"routeXrayPort"`
  367. OutboundTag string `json:"outboundTag"`
  368. }
  369. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
  370. return
  371. }
  372. if !parsed.RouteThroughXray || parsed.RouteXrayPort <= 0 || inbound.Tag == "" {
  373. return
  374. }
  375. tag := inbound.Tag
  376. for i := range cfg.InboundConfigs {
  377. if cfg.InboundConfigs[i].Tag == tag {
  378. logger.Warning("mtproto egress: inbound tag [", tag, "] already present in generated config, skipping bridge")
  379. return
  380. }
  381. }
  382. if parsed.OutboundTag != "" {
  383. routing := map[string]any{}
  384. parseOK := true
  385. if len(cfg.RouterConfig) > 0 {
  386. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  387. logger.Warning("mtproto egress: routing section is unparsable, skipping rule:", err)
  388. parseOK = false
  389. }
  390. }
  391. if parseOK {
  392. rules, _ := routing["rules"].([]any)
  393. rule := map[string]any{
  394. "type": "field",
  395. "inboundTag": []any{tag},
  396. }
  397. if routingTagIsBalancer(routing, parsed.OutboundTag) {
  398. rule["balancerTag"] = parsed.OutboundTag
  399. } else {
  400. rule["outboundTag"] = parsed.OutboundTag
  401. }
  402. routing["rules"] = append([]any{rule}, rules...)
  403. if newRouting, err := json.Marshal(routing); err == nil {
  404. cfg.RouterConfig = json_util.RawMessage(newRouting)
  405. } else {
  406. logger.Warning("mtproto egress: failed to rebuild routing section, skipping rule:", err)
  407. }
  408. }
  409. }
  410. cfg.InboundConfigs = append(cfg.InboundConfigs, xray.InboundConfig{
  411. Listen: json_util.RawMessage(`"127.0.0.1"`),
  412. Port: parsed.RouteXrayPort,
  413. Protocol: "socks",
  414. Settings: json_util.RawMessage(mtprotoEgressSocksSettings),
  415. Tag: tag,
  416. })
  417. }
  418. // mergeSubscriptionOutbounds appends the subscription outbounds to the
  419. // OutboundConfigs array of the xray config. It works on the already-unmarshaled
  420. // template so that manually configured outbounds are never overwritten.
  421. //
  422. // Safety: if we cannot parse the template's outbounds array, we leave
  423. // OutboundConfigs exactly as it came from the template (we do not inject
  424. // subscription outbounds). This prevents us from accidentally dropping the
  425. // user's manually configured outbounds when the template is in a weird state.
  426. func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
  427. if len(prepend) == 0 && len(appendList) == 0 {
  428. return
  429. }
  430. var templateOutbounds []any
  431. if len(cfg.OutboundConfigs) > 0 {
  432. if err := json.Unmarshal(cfg.OutboundConfigs, &templateOutbounds); err != nil {
  433. // Corrupt template outbounds — do not touch the field at all.
  434. // The user will see problems on Xray start / next save.
  435. return
  436. }
  437. }
  438. merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList))
  439. merged = append(merged, prepend...)
  440. merged = append(merged, templateOutbounds...)
  441. merged = append(merged, appendList...)
  442. combined, err := json.MarshalIndent(merged, "", " ")
  443. if err != nil {
  444. return
  445. }
  446. cfg.OutboundConfigs = json_util.RawMessage(combined)
  447. }
  448. // ensureAPIServices guarantees the gRPC services the panel depends on are
  449. // listed in the generated config's api block: HandlerService and StatsService
  450. // have always been required for inbound/user management and traffic polling,
  451. // and RoutingService enables hot routing reload on templates saved before it
  452. // was added to the default template. The stored template itself is not
  453. // modified — only the generated runtime config.
  454. func ensureAPIServices(api json_util.RawMessage) json_util.RawMessage {
  455. if len(api) == 0 {
  456. // No api block means the panel's API integration is deliberately
  457. // disabled; don't resurrect it behind the user's back.
  458. return api
  459. }
  460. var parsed map[string]any
  461. if err := json.Unmarshal(api, &parsed); err != nil {
  462. return api
  463. }
  464. services, _ := parsed["services"].([]any)
  465. have := make(map[string]bool, len(services))
  466. for _, svc := range services {
  467. if name, ok := svc.(string); ok {
  468. have[name] = true
  469. }
  470. }
  471. added := false
  472. for _, name := range []string{"HandlerService", "StatsService", "RoutingService"} {
  473. if !have[name] {
  474. services = append(services, name)
  475. added = true
  476. }
  477. }
  478. if !added {
  479. return api
  480. }
  481. parsed["services"] = services
  482. out, err := json.Marshal(parsed)
  483. if err != nil {
  484. return api
  485. }
  486. return out
  487. }
  488. // ensureStatsPolicy guarantees every policy level in the generated config has
  489. // statsUserOnline enabled, so the core tracks per-email online IPs for the
  490. // panel's online view and access-log-free IP limiting. Generated clients carry
  491. // no explicit level, so level "0" is created when absent. The flag is panel
  492. // infrastructure and is forced on even over an explicit false in the template,
  493. // same as the api services above. An entirely missing or unparsable policy
  494. // block is left alone; the stored template itself is never modified — only the
  495. // generated runtime config.
  496. func ensureStatsPolicy(policy json_util.RawMessage) json_util.RawMessage {
  497. if len(policy) == 0 {
  498. return policy
  499. }
  500. var parsed map[string]any
  501. if err := json.Unmarshal(policy, &parsed); err != nil {
  502. return policy
  503. }
  504. levels, _ := parsed["levels"].(map[string]any)
  505. if levels == nil {
  506. levels = make(map[string]any)
  507. }
  508. if _, ok := levels["0"]; !ok {
  509. levels["0"] = map[string]any{}
  510. }
  511. changed := false
  512. for _, raw := range levels {
  513. level, ok := raw.(map[string]any)
  514. if !ok {
  515. continue
  516. }
  517. if enabled, ok := level["statsUserOnline"].(bool); !ok || !enabled {
  518. level["statsUserOnline"] = true
  519. changed = true
  520. }
  521. }
  522. if !changed {
  523. return policy
  524. }
  525. parsed["levels"] = levels
  526. out, err := json.Marshal(parsed)
  527. if err != nil {
  528. return policy
  529. }
  530. return out
  531. }
  532. func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
  533. if len(logCfg) == 0 {
  534. return logCfg
  535. }
  536. var parsed map[string]any
  537. if err := json.Unmarshal(logCfg, &parsed); err != nil {
  538. return logCfg
  539. }
  540. changed := false
  541. for _, key := range []string{"access", "error"} {
  542. v, ok := parsed[key].(string)
  543. if !ok {
  544. continue
  545. }
  546. trimmed := strings.TrimSpace(v)
  547. if trimmed == "" || strings.EqualFold(trimmed, "none") {
  548. continue
  549. }
  550. base := path.Base(filepath.ToSlash(trimmed))
  551. if base == "" || base == "." || base == ".." || base == "/" {
  552. continue
  553. }
  554. confined := filepath.Join(config.GetLogFolder(), base)
  555. if confined == trimmed {
  556. continue
  557. }
  558. parsed[key] = confined
  559. changed = true
  560. }
  561. if !changed {
  562. return logCfg
  563. }
  564. out, err := json.Marshal(parsed)
  565. if err != nil {
  566. return logCfg
  567. }
  568. return out
  569. }
  570. // GetXrayTraffic fetches the current traffic statistics from the running Xray process.
  571. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
  572. if !s.IsXrayRunning() {
  573. err := errors.New("xray is not running")
  574. logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
  575. return nil, nil, err
  576. }
  577. apiPort := p.GetAPIPort()
  578. if err := s.xrayAPI.Init(apiPort); err != nil {
  579. logger.Debug("Failed to initialize Xray API:", err)
  580. return nil, nil, err
  581. }
  582. defer s.xrayAPI.Close()
  583. traffic, clientTraffic, err := s.xrayAPI.GetTraffic()
  584. if err != nil {
  585. logger.Debug("Failed to fetch Xray traffic:", err)
  586. return nil, nil, err
  587. }
  588. return traffic, clientTraffic, nil
  589. }
  590. // GetOnlineUsers returns connection-based online users (email + source IPs)
  591. // from the running core's online-stats API. ok=false means the API is not
  592. // available — xray isn't running or the core predates the online-stats RPCs —
  593. // and callers must use the legacy traffic-delta / access-log paths. The
  594. // capability is probed lazily per process: an Unimplemented answer pins this
  595. // core as unsupported until the next restart, while transient errors leave the
  596. // capability undecided so a flaky poll can't lock in legacy mode.
  597. func (s *XrayService) GetOnlineUsers() ([]xray.OnlineUser, bool, error) {
  598. if !s.IsXrayRunning() {
  599. return nil, false, nil
  600. }
  601. if p.OnlineAPISupport() == xray.OnlineAPIUnsupported {
  602. return nil, false, nil
  603. }
  604. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  605. logger.Debug("Failed to initialize Xray API:", err)
  606. return nil, false, err
  607. }
  608. defer s.xrayAPI.Close()
  609. users, err := s.xrayAPI.GetOnlineUsers()
  610. if err != nil {
  611. if xray.IsUnimplementedErr(err) {
  612. p.SetOnlineAPISupport(xray.OnlineAPIUnsupported)
  613. logger.Info("xray core does not support the online-stats API; falling back to traffic-delta onlines and access-log IP limit")
  614. return nil, false, nil
  615. }
  616. logger.Debug("Failed to fetch Xray online users:", err)
  617. return nil, false, err
  618. }
  619. if p.OnlineAPISupport() == xray.OnlineAPIUnknown {
  620. p.SetOnlineAPISupport(xray.OnlineAPISupported)
  621. logger.Info("xray core supports the online-stats API; using connection-based onlines and access-log-free IP limit")
  622. }
  623. return users, true, nil
  624. }
  625. // BalancerStatus is the live view of one balancer for the panel UI. Running
  626. // is false when the balancer isn't present in the running core (e.g. xray is
  627. // stopped or the balancer hasn't been saved/applied yet).
  628. type BalancerStatus struct {
  629. Tag string `json:"tag"`
  630. Running bool `json:"running"`
  631. Override string `json:"override"`
  632. Selected []string `json:"selected"`
  633. }
  634. // GetBalancersStatus queries the running core for the live state of the
  635. // given balancer tags. Per-tag failures are reported as Running=false rather
  636. // than failing the whole call, so the UI can render saved-but-not-applied
  637. // balancers alongside live ones.
  638. func (s *XrayService) GetBalancersStatus(tags []string) ([]BalancerStatus, error) {
  639. statuses := make([]BalancerStatus, 0, len(tags))
  640. if !s.IsXrayRunning() {
  641. for _, tag := range tags {
  642. statuses = append(statuses, BalancerStatus{Tag: tag})
  643. }
  644. return statuses, nil
  645. }
  646. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  647. return nil, err
  648. }
  649. defer s.xrayAPI.Close()
  650. for _, tag := range tags {
  651. info, err := s.xrayAPI.GetBalancerInfo(tag)
  652. if err != nil {
  653. logger.Debug("get balancer info [", tag, "] failed:", err)
  654. statuses = append(statuses, BalancerStatus{Tag: tag})
  655. continue
  656. }
  657. statuses = append(statuses, BalancerStatus{
  658. Tag: tag,
  659. Running: true,
  660. Override: info.Override,
  661. Selected: info.Selected,
  662. })
  663. }
  664. return statuses, nil
  665. }
  666. // OverrideBalancer forces a balancer in the running core to use the given
  667. // outbound tag; an empty target clears the override.
  668. func (s *XrayService) OverrideBalancer(tag, target string) error {
  669. if !s.IsXrayRunning() {
  670. return errors.New("xray is not running")
  671. }
  672. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  673. return err
  674. }
  675. defer s.xrayAPI.Close()
  676. return s.xrayAPI.SetBalancerTarget(tag, target)
  677. }
  678. // TestRoute asks the running core which outbound its router picks for the
  679. // described connection.
  680. func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
  681. if !s.IsXrayRunning() {
  682. return nil, errors.New("xray is not running")
  683. }
  684. if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
  685. return nil, err
  686. }
  687. defer s.xrayAPI.Close()
  688. return s.xrayAPI.TestRoute(req)
  689. }
  690. // RestartXray reconciles the running Xray process with the current desired
  691. // config. When isForce is false it first tries to apply the changes through
  692. // the Xray gRPC API without restarting the process (inbounds, outbounds and
  693. // routing rules/balancers are hot-reloadable); only changes the core cannot
  694. // take at runtime — or a force request — stop and restart the process.
  695. func (s *XrayService) RestartXray(isForce bool) error {
  696. lock.Lock()
  697. defer lock.Unlock()
  698. logger.Debug("restart Xray, force:", isForce)
  699. isManuallyStopped.Store(false)
  700. xrayConfig, err := s.GetXrayConfig()
  701. if err != nil {
  702. return err
  703. }
  704. if s.IsXrayRunning() {
  705. configUnchanged := p.GetConfig().Equals(xrayConfig)
  706. if !isForce && configUnchanged && !isNeedXrayRestart.Load() {
  707. logger.Debug("It does not need to restart Xray")
  708. return nil
  709. }
  710. if !isForce && !configUnchanged && s.tryHotApply(xrayConfig) {
  711. logger.Info("Xray config changes applied through the core API, no restart needed")
  712. return nil
  713. }
  714. p.Stop()
  715. }
  716. p = xray.NewProcess(xrayConfig)
  717. result = ""
  718. s.xrayAPI.StatsLastValues = nil
  719. err = p.Start()
  720. if err != nil {
  721. return err
  722. }
  723. return nil
  724. }
  725. // tryHotApply attempts to reconcile the running Xray instance with newCfg
  726. // through the core gRPC API (HandlerService for inbounds/outbounds,
  727. // RoutingService for rules/balancers). It returns true when the running
  728. // instance now matches newCfg; on any failure it returns false and the
  729. // caller falls back to a full process restart, which cleans up whatever was
  730. // partially applied. Callers must hold the package-level lock.
  731. func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
  732. oldCfg := p.GetConfig()
  733. diff, ok := xray.ComputeHotDiff(oldCfg, newCfg)
  734. if !ok {
  735. logger.Debug("hot apply: config change is not API-applicable, falling back to restart")
  736. return false
  737. }
  738. if diff.Empty() {
  739. p.SetConfig(newCfg)
  740. return true
  741. }
  742. apiPort := p.GetAPIPort()
  743. if apiPort <= 0 {
  744. return false
  745. }
  746. // A dedicated client: s.xrayAPI may be in use by traffic polling on other
  747. // service instances and is reset around restarts.
  748. hotAPI := xray.XrayAPI{}
  749. if err := hotAPI.Init(apiPort); err != nil {
  750. logger.Debug("hot apply: failed to init xray api:", err)
  751. return false
  752. }
  753. defer hotAPI.Close()
  754. // Removals first so changed handlers and port swaps never collide with
  755. // the additions that follow.
  756. for _, tag := range diff.RemovedInboundTags {
  757. if err := hotAPI.DelInbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  758. logger.Info("hot apply: remove inbound [", tag, "] failed:", err)
  759. return false
  760. }
  761. }
  762. for _, tag := range diff.RemovedOutboundTags {
  763. if err := hotAPI.DelOutbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
  764. logger.Info("hot apply: remove outbound [", tag, "] failed:", err)
  765. return false
  766. }
  767. }
  768. for _, ob := range diff.AddedOutbounds {
  769. if err := addOutboundReconciling(&hotAPI, ob); err != nil {
  770. logger.Info("hot apply: add outbound failed:", err)
  771. return false
  772. }
  773. }
  774. for _, ib := range diff.AddedInbounds {
  775. if err := addInboundReconciling(&hotAPI, ib); err != nil {
  776. logger.Info("hot apply: add inbound failed:", err)
  777. return false
  778. }
  779. }
  780. if diff.RoutingConfig != nil {
  781. if err := hotAPI.ApplyRoutingConfig(diff.RoutingConfig); err != nil {
  782. logger.Info("hot apply: apply routing config failed:", err)
  783. return false
  784. }
  785. }
  786. p.SetConfig(newCfg)
  787. return true
  788. }
  789. // addInboundReconciling adds an inbound, and on a tag conflict (the handler
  790. // was already created through the runtime API while the stored snapshot was
  791. // stale) replaces the existing handler instead.
  792. func addInboundReconciling(api *xray.XrayAPI, inbound []byte) error {
  793. err := api.AddInbound(inbound)
  794. if err == nil || !xray.IsExistingTagErr(err) {
  795. return err
  796. }
  797. var meta struct {
  798. Tag string `json:"tag"`
  799. }
  800. if jsonErr := json.Unmarshal(inbound, &meta); jsonErr != nil || meta.Tag == "" {
  801. return err
  802. }
  803. if delErr := api.DelInbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  804. return delErr
  805. }
  806. return api.AddInbound(inbound)
  807. }
  808. // addOutboundReconciling mirrors addInboundReconciling for outbounds.
  809. func addOutboundReconciling(api *xray.XrayAPI, outbound []byte) error {
  810. err := api.AddOutbound(outbound)
  811. if err == nil || !xray.IsExistingTagErr(err) {
  812. return err
  813. }
  814. var meta struct {
  815. Tag string `json:"tag"`
  816. }
  817. if jsonErr := json.Unmarshal(outbound, &meta); jsonErr != nil || meta.Tag == "" {
  818. return err
  819. }
  820. if delErr := api.DelOutbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
  821. return delErr
  822. }
  823. return api.AddOutbound(outbound)
  824. }
  825. // StopXray stops the running Xray process.
  826. func (s *XrayService) StopXray() error {
  827. lock.Lock()
  828. defer lock.Unlock()
  829. isManuallyStopped.Store(true)
  830. logger.Debug("Attempting to stop Xray...")
  831. if s.IsXrayRunning() {
  832. return p.Stop()
  833. }
  834. return errors.New("xray is not running")
  835. }
  836. // SetToNeedRestart marks that Xray needs to be restarted.
  837. func (s *XrayService) SetToNeedRestart() {
  838. isNeedXrayRestart.Store(true)
  839. }
  840. // GetXrayAPIPort returns the port the local xray process is listening on
  841. // for its gRPC HandlerService, or 0 when xray isn't currently running.
  842. // Exposed for the runtime package's LocalRuntime adapter — runtime can't
  843. // reach into the package-level `p` directly without a service-package
  844. // import cycle.
  845. func (s *XrayService) GetXrayAPIPort() int {
  846. if p == nil || !p.IsRunning() {
  847. return 0
  848. }
  849. return p.GetAPIPort()
  850. }
  851. // IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
  852. func (s *XrayService) IsNeedRestartAndSetFalse() bool {
  853. return isNeedXrayRestart.CompareAndSwap(true, false)
  854. }
  855. // DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
  856. func (s *XrayService) DidXrayCrash() bool {
  857. return !s.IsXrayRunning() && !isManuallyStopped.Load()
  858. }