process.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. package xray
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "os/exec"
  9. "runtime"
  10. "strings"
  11. "sync"
  12. "sync/atomic"
  13. "syscall"
  14. "time"
  15. "github.com/mhsanaei/3x-ui/v3/config"
  16. "github.com/mhsanaei/3x-ui/v3/logger"
  17. "github.com/mhsanaei/3x-ui/v3/util/common"
  18. )
  19. // GetBinaryName returns the Xray binary filename for the current OS and architecture.
  20. func GetBinaryName() string {
  21. return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
  22. }
  23. // GetBinaryPath returns the full path to the Xray binary executable.
  24. func GetBinaryPath() string {
  25. return config.GetBinFolderPath() + "/" + GetBinaryName()
  26. }
  27. // GetConfigPath returns the path to the Xray configuration file in the binary folder.
  28. func GetConfigPath() string {
  29. return config.GetBinFolderPath() + "/config.json"
  30. }
  31. // GetGeositePath returns the path to the geosite data file used by Xray.
  32. func GetGeositePath() string {
  33. return config.GetBinFolderPath() + "/geosite.dat"
  34. }
  35. // GetGeoipPath returns the path to the geoip data file used by Xray.
  36. func GetGeoipPath() string {
  37. return config.GetBinFolderPath() + "/geoip.dat"
  38. }
  39. // GetIPLimitLogPath returns the path to the IP limit log file.
  40. func GetIPLimitLogPath() string {
  41. return config.GetLogFolder() + "/3xipl.log"
  42. }
  43. // GetIPLimitBannedLogPath returns the path to the banned IP log file.
  44. func GetIPLimitBannedLogPath() string {
  45. return config.GetLogFolder() + "/3xipl-banned.log"
  46. }
  47. // GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file.
  48. func GetIPLimitBannedPrevLogPath() string {
  49. return config.GetLogFolder() + "/3xipl-banned.prev.log"
  50. }
  51. // GetAccessPersistentLogPath returns the path to the persistent access log file.
  52. func GetAccessPersistentLogPath() string {
  53. return config.GetLogFolder() + "/3xipl-ap.log"
  54. }
  55. // GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file.
  56. func GetAccessPersistentPrevLogPath() string {
  57. return config.GetLogFolder() + "/3xipl-ap.prev.log"
  58. }
  59. // GetAccessLogPath reads the Xray config and returns the access log file path.
  60. func GetAccessLogPath() (string, error) {
  61. config, err := os.ReadFile(GetConfigPath())
  62. if err != nil {
  63. logger.Warningf("Failed to read configuration file: %s", err)
  64. return "", err
  65. }
  66. jsonConfig := map[string]any{}
  67. err = json.Unmarshal([]byte(config), &jsonConfig)
  68. if err != nil {
  69. logger.Warningf("Failed to parse JSON configuration: %s", err)
  70. return "", err
  71. }
  72. if jsonConfig["log"] != nil {
  73. jsonLog := jsonConfig["log"].(map[string]any)
  74. if jsonLog["access"] != nil {
  75. accessLogPath := jsonLog["access"].(string)
  76. return accessLogPath, nil
  77. }
  78. }
  79. return "", err
  80. }
  81. // stopProcess calls Stop on the given Process instance.
  82. func stopProcess(p *Process) {
  83. p.Stop()
  84. }
  85. // Process wraps an Xray process instance and provides management methods.
  86. type Process struct {
  87. *process
  88. }
  89. // NewProcess creates a new Xray process and sets up cleanup on garbage collection.
  90. func NewProcess(xrayConfig *Config) *Process {
  91. p := &Process{newProcess(xrayConfig)}
  92. runtime.SetFinalizer(p, stopProcess)
  93. return p
  94. }
  95. // NewTestProcess creates a new Xray process that uses a specific config file path.
  96. // Used for test runs (e.g. outbound test) so the main config.json is not overwritten.
  97. // The config file at configPath is removed when the process is stopped.
  98. func NewTestProcess(xrayConfig *Config, configPath string) *Process {
  99. p := &Process{newTestProcess(xrayConfig, configPath)}
  100. runtime.SetFinalizer(p, stopProcess)
  101. return p
  102. }
  103. type process struct {
  104. cmd *exec.Cmd
  105. done chan struct{}
  106. version string
  107. apiPort int
  108. onlineClients []string
  109. // nodeOnlineClients holds the online-emails list reported by each
  110. // remote node, keyed by node id. NodeTrafficSyncJob populates entries
  111. // per cron tick and clears them when a node's probe fails. The mutex
  112. // guards both this map and onlineClients above so GetOnlineClients
  113. // can build the union without a torn read.
  114. nodeOnlineClients map[int][]string
  115. onlineMu sync.RWMutex
  116. config *Config
  117. configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
  118. logWriter *LogWriter
  119. exitErr error
  120. startTime time.Time
  121. intentionalStop atomic.Bool
  122. }
  123. var (
  124. xrayGracefulStopTimeout = 5 * time.Second
  125. xrayForceStopTimeout = 2 * time.Second
  126. )
  127. // newProcess creates a new internal process struct for Xray.
  128. func newProcess(config *Config) *process {
  129. return &process{
  130. version: "Unknown",
  131. config: config,
  132. logWriter: NewLogWriter(),
  133. startTime: time.Now(),
  134. }
  135. }
  136. // newTestProcess creates a process that writes and runs with a specific config path.
  137. func newTestProcess(config *Config, configPath string) *process {
  138. p := newProcess(config)
  139. p.configPath = configPath
  140. return p
  141. }
  142. // IsRunning returns true if the Xray process is currently running.
  143. func (p *process) IsRunning() bool {
  144. if p.cmd == nil || p.cmd.Process == nil {
  145. return false
  146. }
  147. if p.done != nil {
  148. select {
  149. case <-p.done:
  150. return false
  151. default:
  152. }
  153. }
  154. if p.cmd.ProcessState == nil {
  155. return true
  156. }
  157. return false
  158. }
  159. // GetErr returns the last error encountered by the Xray process.
  160. func (p *process) GetErr() error {
  161. return p.exitErr
  162. }
  163. // GetResult returns the last log line or error from the Xray process.
  164. func (p *process) GetResult() string {
  165. if len(p.logWriter.lastLine) == 0 && p.exitErr != nil {
  166. return p.exitErr.Error()
  167. }
  168. return p.logWriter.lastLine
  169. }
  170. // GetVersion returns the version string of the Xray process.
  171. func (p *process) GetVersion() string {
  172. return p.version
  173. }
  174. // GetAPIPort returns the API port used by the Xray process.
  175. func (p *Process) GetAPIPort() int {
  176. return p.apiPort
  177. }
  178. // GetConfig returns the configuration used by the Xray process.
  179. func (p *Process) GetConfig() *Config {
  180. return p.config
  181. }
  182. // GetOnlineClients returns the union of locally-online clients and
  183. // node-online clients from every registered remote panel. Dedupes by
  184. // email so a client connected to both a local and a node-managed inbound
  185. // surfaces once. Cheap allocation — typical online sets are small and
  186. // the union is recomputed on demand.
  187. func (p *Process) GetOnlineClients() []string {
  188. p.onlineMu.RLock()
  189. defer p.onlineMu.RUnlock()
  190. if len(p.nodeOnlineClients) == 0 {
  191. // Hot path for single-panel deployments: avoid the map+dedupe
  192. // work entirely and return the local slice as-is.
  193. return p.onlineClients
  194. }
  195. seen := make(map[string]struct{}, len(p.onlineClients))
  196. out := make([]string, 0, len(p.onlineClients))
  197. for _, email := range p.onlineClients {
  198. if _, dup := seen[email]; dup {
  199. continue
  200. }
  201. seen[email] = struct{}{}
  202. out = append(out, email)
  203. }
  204. for _, list := range p.nodeOnlineClients {
  205. for _, email := range list {
  206. if _, dup := seen[email]; dup {
  207. continue
  208. }
  209. seen[email] = struct{}{}
  210. out = append(out, email)
  211. }
  212. }
  213. return out
  214. }
  215. // SetOnlineClients sets the locally-online list. Called by the local
  216. // XrayTrafficJob after each xray gRPC stats poll.
  217. func (p *Process) SetOnlineClients(users []string) {
  218. p.onlineMu.Lock()
  219. p.onlineClients = users
  220. p.onlineMu.Unlock()
  221. }
  222. // SetNodeOnlineClients records the online-emails set for one remote
  223. // node. Replaces any previous entry for that node — NodeTrafficSyncJob
  224. // always sends the full list per tick.
  225. func (p *Process) SetNodeOnlineClients(nodeID int, emails []string) {
  226. p.onlineMu.Lock()
  227. defer p.onlineMu.Unlock()
  228. if p.nodeOnlineClients == nil {
  229. p.nodeOnlineClients = map[int][]string{}
  230. }
  231. p.nodeOnlineClients[nodeID] = emails
  232. }
  233. // ClearNodeOnlineClients drops a node's contribution to the online set.
  234. // Called when a probe fails so a downed node doesn't keep its clients
  235. // listed as "online" until the next successful probe.
  236. func (p *Process) ClearNodeOnlineClients(nodeID int) {
  237. p.onlineMu.Lock()
  238. defer p.onlineMu.Unlock()
  239. delete(p.nodeOnlineClients, nodeID)
  240. }
  241. // GetUptime returns the uptime of the Xray process in seconds.
  242. func (p *Process) GetUptime() uint64 {
  243. return uint64(time.Since(p.startTime).Seconds())
  244. }
  245. // refreshAPIPort updates the API port from the inbound configs.
  246. func (p *process) refreshAPIPort() {
  247. for _, inbound := range p.config.InboundConfigs {
  248. if inbound.Tag == "api" {
  249. p.apiPort = inbound.Port
  250. break
  251. }
  252. }
  253. }
  254. // refreshVersion updates the version string by running the Xray binary with -version.
  255. func (p *process) refreshVersion() {
  256. cmd := exec.Command(GetBinaryPath(), "-version")
  257. data, err := cmd.Output()
  258. if err != nil {
  259. p.version = "Unknown"
  260. } else {
  261. datas := bytes.Split(data, []byte(" "))
  262. if len(datas) <= 1 {
  263. p.version = "Unknown"
  264. } else {
  265. p.version = string(datas[1])
  266. }
  267. }
  268. }
  269. // Start launches the Xray process with the current configuration.
  270. func (p *process) Start() (err error) {
  271. if p.IsRunning() {
  272. return errors.New("xray is already running")
  273. }
  274. defer func() {
  275. if err != nil {
  276. logger.Error("Failure in running xray-core process: ", err)
  277. p.exitErr = err
  278. }
  279. }()
  280. data, err := json.MarshalIndent(p.config, "", " ")
  281. if err != nil {
  282. return common.NewErrorf("Failed to generate XRAY configuration files: %v", err)
  283. }
  284. err = os.MkdirAll(config.GetLogFolder(), 0o770)
  285. if err != nil {
  286. logger.Warningf("Failed to create log folder: %s", err)
  287. }
  288. configPath := GetConfigPath()
  289. if p.configPath != "" {
  290. configPath = p.configPath
  291. }
  292. err = os.WriteFile(configPath, data, 0644)
  293. if err != nil {
  294. return common.NewErrorf("Failed to write configuration file: %v", err)
  295. }
  296. cmd := exec.Command(GetBinaryPath(), "-c", configPath)
  297. cmd.Stdout = p.logWriter
  298. cmd.Stderr = p.logWriter
  299. err = p.startCommand(cmd)
  300. if err != nil {
  301. return err
  302. }
  303. p.refreshVersion()
  304. p.refreshAPIPort()
  305. return nil
  306. }
  307. func (p *process) startCommand(cmd *exec.Cmd) error {
  308. p.cmd = cmd
  309. p.done = make(chan struct{})
  310. p.exitErr = nil
  311. p.intentionalStop.Store(false)
  312. if err := cmd.Start(); err != nil {
  313. close(p.done)
  314. p.cmd = nil
  315. return err
  316. }
  317. go p.waitForCommand(cmd)
  318. return nil
  319. }
  320. func (p *process) waitForCommand(cmd *exec.Cmd) {
  321. defer close(p.done)
  322. err := cmd.Wait()
  323. if err == nil || p.intentionalStop.Load() {
  324. return
  325. }
  326. // On Windows, killing the process results in "exit status 1" which isn't an error for us.
  327. if runtime.GOOS == "windows" {
  328. errStr := strings.ToLower(err.Error())
  329. if strings.Contains(errStr, "exit status 1") {
  330. p.exitErr = err
  331. return
  332. }
  333. }
  334. logger.Error("Failure in running xray-core:", err)
  335. p.exitErr = err
  336. }
  337. // Stop terminates the running Xray process.
  338. func (p *process) Stop() error {
  339. if !p.IsRunning() {
  340. return errors.New("xray is not running")
  341. }
  342. p.intentionalStop.Store(true)
  343. // Remove temporary config file used for test runs so main config is never touched
  344. if p.configPath != "" {
  345. if p.configPath != GetConfigPath() {
  346. // Check if file exists before removing
  347. if _, err := os.Stat(p.configPath); err == nil {
  348. _ = os.Remove(p.configPath)
  349. }
  350. }
  351. }
  352. if runtime.GOOS == "windows" {
  353. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  354. return err
  355. }
  356. return p.waitForExit(xrayForceStopTimeout)
  357. }
  358. if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
  359. if errors.Is(err, os.ErrProcessDone) {
  360. return p.waitForExit(xrayForceStopTimeout)
  361. }
  362. return err
  363. }
  364. if err := p.waitForExit(xrayGracefulStopTimeout); err == nil {
  365. return nil
  366. }
  367. logger.Warning("xray-core did not stop after SIGTERM, killing process")
  368. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  369. return err
  370. }
  371. return p.waitForExit(xrayForceStopTimeout)
  372. }
  373. func (p *process) waitForExit(timeout time.Duration) error {
  374. if p.done == nil {
  375. return nil
  376. }
  377. timer := time.NewTimer(timeout)
  378. defer timer.Stop()
  379. select {
  380. case <-p.done:
  381. return nil
  382. case <-timer.C:
  383. return common.NewErrorf("timed out waiting for xray-core process to stop after %s", timeout)
  384. }
  385. }
  386. // writeCrashReport writes a crash report to the binary folder with a timestamped filename.
  387. func writeCrashReport(m []byte) error {
  388. crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log"
  389. return os.WriteFile(crashReportPath, m, 0644)
  390. }