mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-06 03:07:31 +03:00
Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP
This commit is contained in:
140
protocol/limiter/rate/outbound.go
Normal file
140
protocol/limiter/rate/outbound.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package rate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.RateLimiterOutboundOptions](registry, C.TypeRateLimiter, NewOutbound)
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
ctx context.Context
|
||||
outbound adapter.OutboundManager
|
||||
connection adapter.ConnectionManager
|
||||
logger logger.ContextLogger
|
||||
strategy RateStrategy
|
||||
outboundTag string
|
||||
detour adapter.Outbound
|
||||
router *route.Router
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RateLimiterOutboundOptions) (adapter.Outbound, error) {
|
||||
if options.Strategy == "" {
|
||||
return nil, E.New("missing strategy")
|
||||
}
|
||||
if options.Route.Final == "" {
|
||||
return nil, E.New("missing final outbound")
|
||||
}
|
||||
var strategy RateStrategy
|
||||
var err error
|
||||
switch options.Strategy {
|
||||
case "users":
|
||||
usersStrategies := make(map[string]RateStrategy, len(options.Users))
|
||||
for _, user := range options.Users {
|
||||
userStrategy, err := CreateStrategy(user.Strategy, user.ConnectionType, int(user.Count), user.Interval.Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersStrategies[user.Name] = userStrategy
|
||||
}
|
||||
strategy = NewUsersRateStrategy(usersStrategies)
|
||||
case "manager":
|
||||
strategy = NewManagerRateStrategy()
|
||||
default:
|
||||
strategy, err = CreateStrategy(options.Strategy, options.ConnectionType, int(options.Count), options.Interval.Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logFactory := service.FromContext[log.Factory](ctx)
|
||||
r := route.NewRouter(ctx, logFactory, options.Route, option.DNSOptions{})
|
||||
err = r.Initialize(options.Route.Rules, options.Route.RuleSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound := &Outbound{
|
||||
Adapter: outbound.NewAdapter(C.TypeRateLimiter, tag, nil, []string{}),
|
||||
ctx: ctx,
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
connection: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
logger: logger,
|
||||
outboundTag: options.Route.Final,
|
||||
strategy: strategy,
|
||||
router: r,
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) Network() []string {
|
||||
return []string{N.NetworkTCP, N.NetworkUDP}
|
||||
}
|
||||
|
||||
func (h *Outbound) Start() error {
|
||||
detour, loaded := h.outbound.Outbound(h.outboundTag)
|
||||
if !loaded {
|
||||
return E.New("outbound not found: ", h.outboundTag)
|
||||
}
|
||||
h.detour = detour
|
||||
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
|
||||
err := h.router.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if err := h.strategy.request(ctx, adapter.ContextFrom(ctx)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.detour.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if err := h.strategy.request(ctx, adapter.ContextFrom(ctx)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.detour.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (h *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if err := h.strategy.request(ctx, &metadata); err != nil {
|
||||
h.logger.ErrorContext(ctx, err)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if err := h.strategy.request(ctx, &metadata); err != nil {
|
||||
h.logger.ErrorContext(ctx, err)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
return
|
||||
}
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Outbound) GetStrategy() RateStrategy {
|
||||
return h.strategy
|
||||
}
|
||||
196
protocol/limiter/rate/strategy.go
Normal file
196
protocol/limiter/rate/strategy.go
Normal file
@@ -0,0 +1,196 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user