xray.go 37 KB

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