xray.go 53 KB

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