Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP

This commit is contained in:
Sergei Maklagin
2026-05-11 00:59:35 +03:00
parent 652e0baf57
commit 3bd162ed6f
241 changed files with 36409 additions and 4086 deletions

View 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
}

View 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)
}