xray.go 33 KB

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