xray.go 58 KB

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