1
0

sub_balancer.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package service
  2. import (
  3. "math"
  4. "slices"
  5. "strings"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  9. )
  10. var subBalancerStrategies = map[string]struct{}{
  11. "leastLoad": {},
  12. "leastPing": {},
  13. "random": {},
  14. "roundRobin": {},
  15. }
  16. // SubBalancerService manages client-side JSON-subscription balancers; rows
  17. // are read per request by internal/sub, so mutations need no xray restart.
  18. type SubBalancerService struct{}
  19. func (s *SubBalancerService) validate(b *model.SubBalancer) error {
  20. b.Remark = strings.TrimSpace(b.Remark)
  21. if b.Remark == "" {
  22. return common.NewError("balancer remark is required")
  23. }
  24. if len(b.Remark) > 256 {
  25. return common.NewError("balancer remark too long (max 256)")
  26. }
  27. if b.Strategy == "" {
  28. b.Strategy = "random"
  29. }
  30. if _, ok := subBalancerStrategies[b.Strategy]; !ok {
  31. return common.NewError("invalid balancer strategy:", b.Strategy)
  32. }
  33. if len(b.InboundIds) == 0 {
  34. return common.NewError("balancer must select at least one inbound")
  35. }
  36. if err := s.validateWeights(b); err != nil {
  37. return err
  38. }
  39. if b.SortOrder < 1 {
  40. b.SortOrder = 1
  41. }
  42. return nil
  43. }
  44. // validateWeights rejects weights xray cannot honor (non-positive, outside
  45. // float32 range, non-leastLoad strategy) and drops stray inbound ids.
  46. func (s *SubBalancerService) validateWeights(b *model.SubBalancer) error {
  47. if len(b.MemberWeights) == 0 {
  48. b.MemberWeights = nil
  49. return nil
  50. }
  51. if b.Strategy != "leastLoad" {
  52. return common.NewError("balancer weights only apply to the leastLoad strategy")
  53. }
  54. cleaned := make(map[int]float64, len(b.MemberWeights))
  55. for id, weight := range b.MemberWeights {
  56. if !slices.Contains(b.InboundIds, id) {
  57. continue
  58. }
  59. // xray decodes costs as float32; out-of-range values make it reject the
  60. // whole config, and underflow decays to the tag-digit fallback weight.
  61. if weight <= 0 || weight > math.MaxFloat32 || weight < math.SmallestNonzeroFloat32 {
  62. return common.NewError("balancer member weights must be a positive float32 value")
  63. }
  64. cleaned[id] = weight
  65. }
  66. if len(cleaned) == 0 {
  67. cleaned = nil
  68. }
  69. b.MemberWeights = cleaned
  70. return nil
  71. }
  72. // List returns all balancers in subscription order.
  73. func (s *SubBalancerService) List() ([]*model.SubBalancer, error) {
  74. var balancers []*model.SubBalancer
  75. err := database.GetDB().Model(&model.SubBalancer{}).
  76. Order("sort_order asc, id asc").Find(&balancers).Error
  77. return balancers, err
  78. }
  79. func (s *SubBalancerService) Get(id int) (*model.SubBalancer, error) {
  80. var balancer model.SubBalancer
  81. if err := database.GetDB().First(&balancer, id).Error; err != nil {
  82. return nil, err
  83. }
  84. return &balancer, nil
  85. }
  86. func (s *SubBalancerService) Create(balancer *model.SubBalancer) (*model.SubBalancer, error) {
  87. if err := s.validate(balancer); err != nil {
  88. return nil, err
  89. }
  90. if err := database.GetDB().Create(balancer).Error; err != nil {
  91. return nil, err
  92. }
  93. return balancer, nil
  94. }
  95. func (s *SubBalancerService) Update(id int, balancer *model.SubBalancer, enabled *bool) (*model.SubBalancer, error) {
  96. if err := s.validate(balancer); err != nil {
  97. return nil, err
  98. }
  99. current, err := s.Get(id)
  100. if err != nil {
  101. return nil, err
  102. }
  103. current.Remark = balancer.Remark
  104. current.Strategy = balancer.Strategy
  105. current.InboundIds = balancer.InboundIds
  106. current.MemberWeights = balancer.MemberWeights
  107. current.SortOrder = balancer.SortOrder
  108. if enabled != nil {
  109. current.Enabled = *enabled
  110. }
  111. if err := database.GetDB().Save(current).Error; err != nil {
  112. return nil, err
  113. }
  114. return current, nil
  115. }
  116. func (s *SubBalancerService) Delete(id int) error {
  117. res := database.GetDB().Delete(&model.SubBalancer{}, id)
  118. if res.Error != nil {
  119. return res.Error
  120. }
  121. if res.RowsAffected == 0 {
  122. return common.NewError("sub balancer not found")
  123. }
  124. return nil
  125. }