Files
sing-box-extended/protocol/limiter/rate/strategy.go

197 lines
4.9 KiB
Go

package rate
import (
"context"
"strconv"
"strings"
"sync"
"time"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/shtorm-7/go-cache/v2"
)
type (
ConnIDGetter = func(context.Context, *adapter.InboundContext) (string, bool)
RateGetter = func(id string) error
RateStrategy interface {
request(ctx context.Context, metadata *adapter.InboundContext) error
}
)
type DefaultRateStrategy struct {
limiter core.Limiter
queue chan struct{}
}
func NewDefaultRateStrategy(strategy string, count int, interval time.Duration) (*DefaultRateStrategy, error) {
limiter, err := gorl.New(core.Config{
Strategy: core.StrategyType(strings.ReplaceAll(strategy, "-", "_")),
Limit: count,
Window: interval,
})
if err != nil {
return nil, err
}
return &DefaultRateStrategy{limiter: limiter, queue: make(chan struct{}, 1)}, nil
}
func (s *DefaultRateStrategy) request(ctx context.Context, metadata *adapter.InboundContext) error {
select {
case s.queue <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
defer func() {
<-s.queue
}()
r, err := s.limiter.Allow(ctx, metadata.Destination.String())
if err != nil {
return err
}
if !r.Allowed {
select {
case <-time.After(r.RetryAfter):
_, err = s.limiter.Allow(ctx, metadata.Destination.String())
return err
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *DefaultRateStrategy) close() error {
return s.limiter.Close()
}
type ConnectionRateStrategy struct {
connIDGetter ConnIDGetter
createStrategy func() (*DefaultRateStrategy, error)
limiters *cache.Cache[string, *DefaultRateStrategy]
mtx sync.Mutex
}
func NewConnectionRateStrategy(connIDGetter ConnIDGetter, strategy string, count int, interval time.Duration) (*ConnectionRateStrategy, error) {
limiters := cache.New[string, *DefaultRateStrategy](interval, time.Second)
limiters.OnEvicted(func(s string, strategy *DefaultRateStrategy) {
strategy.close()
})
return &ConnectionRateStrategy{
connIDGetter: connIDGetter,
createStrategy: func() (*DefaultRateStrategy, error) {
return NewDefaultRateStrategy(strategy, count, interval)
},
limiters: limiters,
}, nil
}
func (s *ConnectionRateStrategy) request(ctx context.Context, metadata *adapter.InboundContext) error {
id, ok := s.connIDGetter(ctx, metadata)
if !ok {
return E.New("id not found")
}
s.mtx.Lock()
strategy, ok := s.limiters.Get(id)
if !ok {
newStrategy, err := s.createStrategy()
if err != nil {
return err
}
s.limiters.SetDefault(id, newStrategy)
strategy = newStrategy
} else {
s.limiters.UpdateExpirationDefault(id)
}
s.mtx.Unlock()
return strategy.request(ctx, metadata)
}
type UsersRateStrategy struct {
strategies map[string]RateStrategy
mtx sync.Mutex
}
func NewUsersRateStrategy(strategies map[string]RateStrategy) *UsersRateStrategy {
return &UsersRateStrategy{
strategies: strategies,
}
}
func (s *UsersRateStrategy) request(ctx context.Context, metadata *adapter.InboundContext) error {
s.mtx.Lock()
defer s.mtx.Unlock()
var user string
if metadata != nil {
user = metadata.User
}
strategy, ok := s.strategies[user]
if ok {
return strategy.request(ctx, metadata)
}
return E.New("user strategy not found: ", user)
}
type ManagerRateStrategy struct {
*UsersRateStrategy
}
func NewManagerRateStrategy() *ManagerRateStrategy {
return &ManagerRateStrategy{
UsersRateStrategy: NewUsersRateStrategy(map[string]RateStrategy{}),
}
}
func (s *ManagerRateStrategy) UpdateStrategies(strategies map[string]RateStrategy) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.strategies = strategies
}
type BypassRateStrategy struct{}
func NewBypassRateStrategy() *BypassRateStrategy {
return &BypassRateStrategy{}
}
func (s *BypassRateStrategy) request(ctx context.Context, metadata *adapter.InboundContext) error {
return nil
}
func CreateStrategy(strategy string, connectionType string, count int, interval time.Duration) (RateStrategy, error) {
if strategy == "bypass" {
return NewBypassRateStrategy(), nil
}
var connIDGetter ConnIDGetter
switch connectionType {
case "hwid":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := ctx.Value("hwid").(string)
return id, ok
}
case "mux":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
id, ok := log.MuxIDFromContext(ctx)
if !ok {
return "", ok
}
return strconv.FormatUint(uint64(id.ID), 10), ok
}
case "source_ip":
connIDGetter = func(ctx context.Context, metadata *adapter.InboundContext) (string, bool) {
return metadata.Source.IPAddr().String(), true
}
case "default", "":
return NewDefaultRateStrategy(strategy, count, interval)
default:
return nil, E.New("connection type not found: ", connectionType)
}
return NewConnectionRateStrategy(connIDGetter, strategy, count, interval)
}