3x-ui/web/service/xray.go

446 lines
12 KiB
Go
Raw Normal View History

2023-02-09 19:18:06 +00:00
package service
import (
"encoding/json"
"errors"
2026-01-05 21:12:53 +00:00
"fmt"
"runtime"
2023-02-09 19:18:06 +00:00
"sync"
2026-01-05 21:12:53 +00:00
"github.com/mhsanaei/3x-ui/v2/database/model"
2025-09-19 08:05:43 +00:00
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/xray"
2023-02-18 12:37:32 +00:00
2023-02-09 19:18:06 +00:00
"go.uber.org/atomic"
)
var (
p *xray.Process
lock sync.Mutex
isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
result string
)
2023-02-09 19:18:06 +00:00
2025-09-20 07:35:50 +00:00
// XrayService provides business logic for Xray process management.
// It handles starting, stopping, restarting Xray, and managing its configuration.
2026-01-05 21:12:53 +00:00
// In multi-node mode, it sends configurations to nodes instead of running Xray locally.
2023-02-09 19:18:06 +00:00
type XrayService struct {
inboundService InboundService
settingService SettingService
2026-01-05 21:12:53 +00:00
nodeService NodeService
xrayAPI xray.XrayAPI
2023-02-09 19:18:06 +00:00
}
2025-09-20 07:35:50 +00:00
// IsXrayRunning checks if the Xray process is currently running.
2023-02-09 19:18:06 +00:00
func (s *XrayService) IsXrayRunning() bool {
return p != nil && p.IsRunning()
}
2025-09-20 07:35:50 +00:00
// GetXrayErr returns the error from the Xray process, if any.
2023-02-09 19:18:06 +00:00
func (s *XrayService) GetXrayErr() error {
if p == nil {
return nil
}
err := p.GetErr()
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return nil
}
return err
2023-02-09 19:18:06 +00:00
}
2025-09-20 07:35:50 +00:00
// GetXrayResult returns the result string from the Xray process.
2023-02-09 19:18:06 +00:00
func (s *XrayService) GetXrayResult() string {
if result != "" {
return result
}
if s.IsXrayRunning() {
return ""
}
if p == nil {
return ""
}
2023-02-09 19:18:06 +00:00
result = p.GetResult()
if runtime.GOOS == "windows" && result == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return ""
}
2023-02-09 19:18:06 +00:00
return result
}
2025-09-20 07:35:50 +00:00
// GetXrayVersion returns the version of the running Xray process.
2023-02-09 19:18:06 +00:00
func (s *XrayService) GetXrayVersion() string {
if p == nil {
return "Unknown"
}
return p.GetVersion()
}
2023-02-18 12:37:32 +00:00
2025-09-20 07:35:50 +00:00
// RemoveIndex removes an element at the specified index from a slice.
// Returns a new slice with the element removed.
func RemoveIndex(s []any, index int) []any {
2023-02-09 19:18:06 +00:00
return append(s[:index], s[index+1:]...)
}
2025-09-20 07:35:50 +00:00
// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
2023-02-09 19:18:06 +00:00
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
templateConfig, err := s.settingService.GetXrayConfigTemplate()
if err != nil {
return nil, err
}
xrayConfig := &xray.Config{}
err = json.Unmarshal([]byte(templateConfig), xrayConfig)
if err != nil {
return nil, err
}
s.inboundService.AddTraffic(nil, nil)
2023-02-09 19:18:06 +00:00
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
return nil, err
}
for _, inbound := range inbounds {
if !inbound.Enable {
continue
}
// get settings clients
settings := map[string]any{}
2023-02-09 19:18:06 +00:00
json.Unmarshal([]byte(inbound.Settings), &settings)
clients, ok := settings["clients"].([]any)
2023-02-09 19:18:06 +00:00
if ok {
// check users active or not
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
indexDecrease := 0
2023-02-09 19:18:06 +00:00
for index, client := range clients {
c := client.(map[string]any)
2023-02-09 19:18:06 +00:00
if c["email"] == clientTraffic.Email {
2023-02-18 12:37:32 +00:00
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease)
indexDecrease++
2024-07-08 21:08:00 +00:00
logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c["email"])
2023-02-09 19:18:06 +00:00
}
}
}
}
// clear client config for additional parameters
var final_clients []any
for _, client := range clients {
c := client.(map[string]any)
if c["enable"] != nil {
if enable, ok := c["enable"].(bool); ok && !enable {
continue
}
}
for key := range c {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, any(c))
}
settings["clients"] = final_clients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
2023-02-09 19:18:06 +00:00
if err != nil {
return nil, err
}
2023-02-18 12:37:32 +00:00
2023-02-09 19:18:06 +00:00
inbound.Settings = string(modifiedSettings)
}
if len(inbound.StreamSettings) > 0 {
// Unmarshal stream JSON
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
// Remove the "settings" field under "tlsSettings" and "realitySettings"
tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
realitySettings, ok2 := stream["realitySettings"].(map[string]any)
if ok1 || ok2 {
if ok1 {
delete(tlsSettings, "settings")
} else if ok2 {
delete(realitySettings, "settings")
}
}
delete(stream, "externalProxy")
newStream, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return nil, err
}
inbound.StreamSettings = string(newStream)
}
2023-02-09 19:18:06 +00:00
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}
return xrayConfig, nil
}
2025-09-20 07:35:50 +00:00
// GetXrayTraffic fetches the current traffic statistics from the running Xray process.
2023-02-09 19:18:06 +00:00
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
if !s.IsXrayRunning() {
2024-07-08 21:08:00 +00:00
err := errors.New("xray is not running")
logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
return nil, nil, err
2023-02-09 19:18:06 +00:00
}
2024-07-08 21:08:00 +00:00
apiPort := p.GetAPIPort()
s.xrayAPI.Init(apiPort)
defer s.xrayAPI.Close()
2024-07-08 21:08:00 +00:00
traffic, clientTraffic, err := s.xrayAPI.GetTraffic(true)
if err != nil {
logger.Debug("Failed to fetch Xray traffic:", err)
return nil, nil, err
}
return traffic, clientTraffic, nil
2023-02-09 19:18:06 +00:00
}
2025-09-20 07:35:50 +00:00
// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.
2026-01-05 21:12:53 +00:00
// In multi-node mode, it sends configurations to nodes instead of restarting local Xray.
2023-02-09 19:18:06 +00:00
func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock()
defer lock.Unlock()
logger.Debug("restart Xray, force:", isForce)
isManuallyStopped.Store(false)
2023-02-09 19:18:06 +00:00
2026-01-05 21:12:53 +00:00
// Check if multi-node mode is enabled
multiMode, err := s.settingService.GetMultiNodeMode()
if err != nil {
multiMode = false // Default to single mode on error
}
if multiMode {
return s.restartXrayMultiMode(isForce)
}
// Single mode: use local Xray
2023-02-09 19:18:06 +00:00
xrayConfig, err := s.GetXrayConfig()
if err != nil {
return err
}
if s.IsXrayRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {
logger.Debug("It does not need to restart Xray")
2023-02-09 19:18:06 +00:00
return nil
}
p.Stop()
}
p = xray.NewProcess(xrayConfig)
result = ""
err = p.Start()
if err != nil {
return err
}
return nil
2023-02-09 19:18:06 +00:00
}
2026-01-05 21:12:53 +00:00
// restartXrayMultiMode handles Xray restart in multi-node mode by sending configs to nodes.
func (s *XrayService) restartXrayMultiMode(isForce bool) error {
// Initialize nodeService if not already initialized
if s.nodeService == (NodeService{}) {
s.nodeService = NodeService{}
}
// Get all nodes
nodes, err := s.nodeService.GetAllNodes()
if err != nil {
return fmt.Errorf("failed to get nodes: %w", err)
}
// Group inbounds by node
nodeInbounds := make(map[int][]*model.Inbound)
allInbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
return fmt.Errorf("failed to get inbounds: %w", err)
}
// Get template config
templateConfig, err := s.settingService.GetXrayConfigTemplate()
if err != nil {
return err
}
baseConfig := &xray.Config{}
if err := json.Unmarshal([]byte(templateConfig), baseConfig); err != nil {
return err
}
// Group inbounds by their assigned nodes
for _, inbound := range allInbounds {
if !inbound.Enable {
continue
}
// Get all nodes assigned to this inbound (multi-node support)
nodes, err := s.nodeService.GetNodesForInbound(inbound.Id)
if err != nil || len(nodes) == 0 {
// Inbound not assigned to any node, skip it (this is normal - not all inbounds need to be assigned)
logger.Debugf("Inbound %d is not assigned to any node, skipping", inbound.Id)
continue
}
// Add inbound to all assigned nodes
for _, node := range nodes {
nodeInbounds[node.Id] = append(nodeInbounds[node.Id], inbound)
}
}
// Send config to each node
for _, node := range nodes {
inbounds, ok := nodeInbounds[node.Id]
if !ok {
// No inbounds assigned to this node, skip
continue
}
// Build config for this node
nodeConfig := *baseConfig
2026-01-05 23:27:12 +00:00
// Preserve API inbound from template (if exists)
apiInbound := xray.InboundConfig{}
hasAPIInbound := false
for _, inbound := range baseConfig.InboundConfigs {
if inbound.Tag == "api" {
apiInbound = inbound
hasAPIInbound = true
break
}
}
2026-01-05 21:12:53 +00:00
nodeConfig.InboundConfigs = []xray.InboundConfig{}
2026-01-05 23:27:12 +00:00
// Add API inbound first if it exists
if hasAPIInbound {
nodeConfig.InboundConfigs = append(nodeConfig.InboundConfigs, apiInbound)
}
2026-01-05 21:12:53 +00:00
for _, inbound := range inbounds {
// Process clients (same logic as GetXrayConfig)
settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients, ok := settings["clients"].([]any)
if ok {
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
indexDecrease := 0
for index, client := range clients {
c := client.(map[string]any)
if c["email"] == clientTraffic.Email {
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease)
indexDecrease++
}
}
}
}
var final_clients []any
for _, client := range clients {
c := client.(map[string]any)
if c["enable"] != nil {
if enable, ok := c["enable"].(bool); ok && !enable {
continue
}
}
for key := range c {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, any(c))
}
settings["clients"] = final_clients
modifiedSettings, _ := json.MarshalIndent(settings, "", " ")
inbound.Settings = string(modifiedSettings)
}
if len(inbound.StreamSettings) > 0 {
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
realitySettings, ok2 := stream["realitySettings"].(map[string]any)
if ok1 || ok2 {
if ok1 {
delete(tlsSettings, "settings")
} else if ok2 {
delete(realitySettings, "settings")
}
}
delete(stream, "externalProxy")
newStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(newStream)
}
inboundConfig := inbound.GenXrayInboundConfig()
nodeConfig.InboundConfigs = append(nodeConfig.InboundConfigs, *inboundConfig)
}
// Marshal config to JSON
configJSON, err := json.MarshalIndent(&nodeConfig, "", " ")
if err != nil {
logger.Errorf("[Node: %s] Failed to marshal config: %v", node.Name, err)
2026-01-05 21:12:53 +00:00
continue
}
// Send to node
if err := s.nodeService.ApplyConfigToNode(node, configJSON); err != nil {
logger.Errorf("[Node: %s] Failed to apply config: %v", node.Name, err)
2026-01-05 21:12:53 +00:00
// Continue with other nodes even if one fails
} else {
logger.Infof("[Node: %s] Successfully applied config", node.Name)
2026-01-05 21:12:53 +00:00
}
}
return nil
}
2025-09-20 07:35:50 +00:00
// StopXray stops the running Xray process.
2023-02-09 19:18:06 +00:00
func (s *XrayService) StopXray() error {
lock.Lock()
defer lock.Unlock()
isManuallyStopped.Store(true)
2024-07-08 21:08:00 +00:00
logger.Debug("Attempting to stop Xray...")
2023-02-09 19:18:06 +00:00
if s.IsXrayRunning() {
return p.Stop()
}
return errors.New("xray is not running")
}
2025-09-20 07:35:50 +00:00
// SetToNeedRestart marks that Xray needs to be restarted.
2023-02-09 19:18:06 +00:00
func (s *XrayService) SetToNeedRestart() {
isNeedXrayRestart.Store(true)
}
2025-09-20 07:35:50 +00:00
// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
2023-02-09 19:18:06 +00:00
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
2023-03-17 16:07:49 +00:00
return isNeedXrayRestart.CompareAndSwap(true, false)
2023-02-09 19:18:06 +00:00
}
2025-09-20 07:35:50 +00:00
// DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
}