Y-Panel/web/service/xray.go
2025-09-05 21:33:48 +08:00

328 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"encoding/json"
"errors"
"runtime"
"sync"
"strconv"
"x-ui/logger"
"x-ui/xray"
json_util "x-ui/util/json_util"
"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
)
type XrayService struct {
inboundService InboundService
settingService SettingService
xrayAPI xray.XrayAPI
}
// IsXrayRunning 检查 Xray 是否正在运行
func (s *XrayService) IsXrayRunning() bool {
return p != nil && p.IsRunning()
}
// 中文注释:
// 新增 GetApiPort 函数。
// 这个函数的作用是安全地返回当前 Xray 进程正在监听的 API 端口号。
// 如果 Xray 没有运行 (p == nil),则返回 0。
// 我们的后台任务将调用这个函数来获取端口号。
func (s *XrayService) GetApiPort() int {
if p == nil {
return 0
}
return p.GetAPIPort()
}
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
}
func (s *XrayService) GetXrayResult() string {
if result != "" {
return result
}
if s.IsXrayRunning() {
return ""
}
if p == nil {
return ""
}
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 ""
}
return result
}
func (s *XrayService) GetXrayVersion() string {
if p == nil {
return "Unknown"
}
return p.GetVersion()
}
func RemoveIndex(s []any, index int) []any {
return append(s[:index], s[index+1:]...)
}
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)
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
return nil, err
}
// =================================================================
// 中文注释: 动态限速核心逻辑 - 第一步: 收集所有限速值
// =================================================================
// 创建一个 map 用于存储所有出现过的、不为0的限速值
uniqueSpeeds := make(map[int]bool)
for _, inbound := range inbounds {
if !inbound.Enable {
continue
}
// 获取该入站下的所有客户端设置
clients, _ := s.inboundService.GetClients(inbound)
for _, client := range clients {
if client.SpeedLimit > 0 {
uniqueSpeeds[client.SpeedLimit] = true
}
}
}
// =================================================================
// 中文注释: 动态限速核心逻辑 - 第二步: 根据收集到的限速值,动态生成 Policy Levels
// =================================================================
// 初始化 policy levels并加入默认的 level 0 (不限速)
policyLevels := make(map[string]interface{})
policyLevels["0"] = map[string]interface{}{"handshake": 8, "connIdle": 500}
// 遍历所有收集到的限速值
for speed := range uniqueSpeeds {
// 为每个速率创建一个 levellevel 的名字就是速率的字符串形式
// 例如,速率 1024 KB/s 对应 level "1024"
policyLevels[strconv.Itoa(speed)] = map[string]interface{}{
"downlinkOnly": speed, // 限制下载速度
"uplinkOnly": speed, // 同时限制上传速度 (您可以根据需要调整)
}
}
// 将生成的 policy 应用到 Xray 配置中
policyJSON, err := json.Marshal(map[string]interface{}{
"levels": policyLevels,
})
if err != nil {
return nil, err
}
xrayConfig.Policy = json_util.RawMessage(policyJSON)
// =================================================================
// 中文注释: 动态限速核心逻辑 - 第三步: 为设置了限速的用户分配对应的 Level
// =================================================================
for _, inbound := range inbounds {
if !inbound.Enable {
continue
}
// get settings clients
settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients, ok := settings["clients"].([]any)
if ok {
// check users active or not
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++
logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c["email"])
}
}
}
}
// clear client config for additional parameters
var final_clients []any
for _, client := range clients {
c := client.(map[string]any)
if enable, ok := c["enable"].(bool); ok && !enable { continue }
// =================================================================
// 这里的逻辑是准备将 client 对象提交给 Xray-core。
// 我们需要将 speedLimit 转换为 Xray 认识的 level 字段。
// 并且,我们不再删除任何字段,因为 Xray-core 会自动忽略它不认识的字段。
// 这样可以确保包含 speedLimit 的完整用户信息被用于生成配置。
// =================================================================
if speedLimit, ok := c["speedLimit"].(float64); ok && speedLimit > 0 {
c["level"] = int(speedLimit)
// 【新增功能】在这里添加日志记录
if email, emailOk := c["email"].(string); emailOk {
logger.Infof("为用户 %s 应用〔独立限速〕: %d KB/s", email, int(speedLimit))
}
} else {
c["level"] = 0
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
final_clients = append(final_clients, c)
}
settings["clients"] = final_clients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
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)
}
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}
return xrayConfig, nil
}
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
if !s.IsXrayRunning() {
err := errors.New("xray is not running")
logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
return nil, nil, err
}
apiPort := p.GetAPIPort()
s.xrayAPI.Init(apiPort)
defer s.xrayAPI.Close()
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
}
func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock()
defer lock.Unlock()
logger.Debug("restart Xray, force:", isForce)
isManuallyStopped.Store(false)
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")
return nil
}
p.Stop()
}
p = xray.NewProcess(xrayConfig)
result = ""
err = p.Start()
if err != nil {
return err
}
return nil
}
func (s *XrayService) StopXray() error {
lock.Lock()
defer lock.Unlock()
isManuallyStopped.Store(true)
logger.Debug("Attempting to stop Xray...")
if s.IsXrayRunning() {
return p.Stop()
}
return errors.New("xray is not running")
}
func (s *XrayService) SetToNeedRestart() {
isNeedXrayRestart.Store(true)
}
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CompareAndSwap(true, false)
}
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
}