fa708f4b5f
Add package and exported symbol doc comments across conn, generator, limiter, and wrapper packages. Fix nil receiver guard in connLimitSingleGenerator.Limiter, add missing httpLoader.Close call, return context.Background instead of nil in serverConn.Context, return explicit error on connection limit exceeded, and simplify Allow logic.
372 lines
8.1 KiB
Go
372 lines
8.1 KiB
Go
// Package conn implements connection limiters that control the number of
|
|
// concurrent connections per key (global, per-IP, or per-CIDR). Limits can
|
|
// be loaded from static configuration, files, Redis, or HTTP endpoints with
|
|
// periodic hot-reload support.
|
|
package conn
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
limiter "github.com/go-gost/core/limiter/conn"
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/x/internal/loader"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
"github.com/yl2chen/cidranger"
|
|
)
|
|
|
|
const (
|
|
// GlobalLimitKey is the special key for a global connection limit that applies
|
|
// to all connections regardless of their source IP.
|
|
GlobalLimitKey = "$"
|
|
// IPLimitKey is the special key for a per-IP connection limit that applies
|
|
// as a default when no specific IP or CIDR rule matches.
|
|
IPLimitKey = "$$"
|
|
)
|
|
|
|
type options struct {
|
|
limits []string
|
|
fileLoader loader.Loader
|
|
redisLoader loader.Loader
|
|
httpLoader loader.Loader
|
|
period time.Duration
|
|
logger logger.Logger
|
|
}
|
|
|
|
type Option func(opts *options)
|
|
|
|
// LimitsOption sets the static limit rules.
|
|
// Each rule is a string in the form "key limit", e.g. "192.168.1.0/24 100".
|
|
func LimitsOption(limits ...string) Option {
|
|
return func(opts *options) {
|
|
opts.limits = limits
|
|
}
|
|
}
|
|
|
|
// ReloadPeriodOption sets the period for hot-reloading limit rules from
|
|
// external loaders (file, Redis, HTTP). A period <= 0 disables periodic reload.
|
|
func ReloadPeriodOption(period time.Duration) Option {
|
|
return func(opts *options) {
|
|
opts.period = period
|
|
}
|
|
}
|
|
|
|
// FileLoaderOption sets the file loader for reading limit rules from a file.
|
|
func FileLoaderOption(fileLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.fileLoader = fileLoader
|
|
}
|
|
}
|
|
|
|
// RedisLoaderOption sets the Redis loader for reading limit rules from Redis.
|
|
func RedisLoaderOption(redisLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.redisLoader = redisLoader
|
|
}
|
|
}
|
|
|
|
// HTTPLoaderOption sets the HTTP loader for reading limit rules from an HTTP endpoint.
|
|
func HTTPLoaderOption(httpLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.httpLoader = httpLoader
|
|
}
|
|
}
|
|
|
|
// LoggerOption sets the logger for the connection limiter.
|
|
func LoggerOption(logger logger.Logger) Option {
|
|
return func(opts *options) {
|
|
opts.logger = logger
|
|
}
|
|
}
|
|
|
|
type connLimiter struct {
|
|
ipLimits map[string]ConnLimitGenerator
|
|
cidrLimits cidranger.Ranger
|
|
limits map[string]limiter.Limiter
|
|
mu sync.Mutex
|
|
cancelFunc context.CancelFunc
|
|
options options
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewConnLimiter creates a new ConnLimiter with the given options.
|
|
// It starts a background goroutine for periodic reload of limit rules.
|
|
func NewConnLimiter(opts ...Option) limiter.ConnLimiter {
|
|
var options options
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
lim := &connLimiter{
|
|
ipLimits: make(map[string]ConnLimitGenerator),
|
|
cidrLimits: cidranger.NewPCTrieRanger(),
|
|
limits: make(map[string]limiter.Limiter),
|
|
options: options,
|
|
cancelFunc: cancel,
|
|
logger: options.logger,
|
|
}
|
|
if lim.logger == nil {
|
|
lim.logger = xlogger.Nop()
|
|
}
|
|
|
|
go lim.periodReload(ctx)
|
|
|
|
return lim
|
|
}
|
|
|
|
func (l *connLimiter) Limiter(key string) limiter.Limiter {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if lim, ok := l.limits[key]; ok {
|
|
return lim
|
|
}
|
|
|
|
var lims []limiter.Limiter
|
|
|
|
if ip := net.ParseIP(key); ip != nil {
|
|
found := false
|
|
if p := l.ipLimits[key]; p != nil {
|
|
if lim := p.Limiter(); lim != nil {
|
|
lims = append(lims, lim)
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
if p, _ := l.cidrLimits.ContainingNetworks(ip); len(p) > 0 {
|
|
if v, _ := p[0].(*cidrLimitEntry); v != nil {
|
|
if lim := v.limit.Limiter(); lim != nil {
|
|
lims = append(lims, lim)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(lims) == 0 {
|
|
if p := l.ipLimits[IPLimitKey]; p != nil {
|
|
if lim := p.Limiter(); lim != nil {
|
|
lims = append(lims, lim)
|
|
}
|
|
}
|
|
}
|
|
|
|
if p := l.ipLimits[GlobalLimitKey]; p != nil {
|
|
if lim := p.Limiter(); lim != nil {
|
|
lims = append(lims, lim)
|
|
}
|
|
}
|
|
|
|
if len(lims) == 0 {
|
|
return nil
|
|
}
|
|
|
|
lim := newLimiterGroup(lims...)
|
|
l.limits[key] = lim
|
|
|
|
if l.logger != nil {
|
|
l.logger.Debugf("conn limit for %s: %d", key, lim.Limit())
|
|
}
|
|
|
|
return lim
|
|
}
|
|
|
|
func (l *connLimiter) periodReload(ctx context.Context) error {
|
|
if err := l.reload(ctx); err != nil {
|
|
l.logger.Warnf("reload: %v", err)
|
|
}
|
|
|
|
period := l.options.period
|
|
if period <= 0 {
|
|
return nil
|
|
}
|
|
if period < time.Second {
|
|
period = time.Second
|
|
}
|
|
|
|
ticker := time.NewTicker(period)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if err := l.reload(ctx); err != nil {
|
|
l.logger.Warnf("reload: %v", err)
|
|
// return err
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *connLimiter) reload(ctx context.Context) error {
|
|
v, err := l.load(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lines := append(l.options.limits, v...)
|
|
|
|
ipLimits := make(map[string]ConnLimitGenerator)
|
|
cidrLimits := cidranger.NewPCTrieRanger()
|
|
|
|
for _, s := range lines {
|
|
key, limit := l.parseLimit(s)
|
|
if key == "" || limit <= 0 {
|
|
continue
|
|
}
|
|
switch key {
|
|
case GlobalLimitKey:
|
|
ipLimits[key] = NewConnLimitSingleGenerator(limit)
|
|
case IPLimitKey:
|
|
ipLimits[key] = NewConnLimitGenerator(limit)
|
|
default:
|
|
if ip := net.ParseIP(key); ip != nil {
|
|
ipLimits[key] = NewConnLimitSingleGenerator(limit)
|
|
break
|
|
}
|
|
if _, ipNet, _ := net.ParseCIDR(key); ipNet != nil {
|
|
cidrLimits.Insert(&cidrLimitEntry{
|
|
ipNet: *ipNet,
|
|
limit: NewConnLimitGenerator(limit),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
l.ipLimits = ipLimits
|
|
l.cidrLimits = cidrLimits
|
|
l.limits = make(map[string]limiter.Limiter)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *connLimiter) load(ctx context.Context) (patterns []string, _ error) {
|
|
if l.options.fileLoader != nil {
|
|
if lister, ok := l.options.fileLoader.(loader.Lister); ok {
|
|
list, er := lister.List(ctx)
|
|
if er != nil {
|
|
l.logger.Warnf("file loader: %v", er)
|
|
}
|
|
for _, s := range list {
|
|
if line := l.parseLine(s); line != "" {
|
|
patterns = append(patterns, line)
|
|
}
|
|
}
|
|
} else {
|
|
r, er := l.options.fileLoader.Load(ctx)
|
|
if er != nil {
|
|
l.logger.Warnf("file loader: %v", er)
|
|
}
|
|
if v, _ := l.parsePatterns(r); v != nil {
|
|
patterns = append(patterns, v...)
|
|
}
|
|
}
|
|
}
|
|
if l.options.redisLoader != nil {
|
|
if lister, ok := l.options.redisLoader.(loader.Lister); ok {
|
|
list, er := lister.List(ctx)
|
|
if er != nil {
|
|
l.logger.Warnf("redis loader: %v", er)
|
|
}
|
|
patterns = append(patterns, list...)
|
|
} else {
|
|
r, er := l.options.redisLoader.Load(ctx)
|
|
if er != nil {
|
|
l.logger.Warnf("redis loader: %v", er)
|
|
}
|
|
if v, _ := l.parsePatterns(r); v != nil {
|
|
patterns = append(patterns, v...)
|
|
}
|
|
}
|
|
}
|
|
if l.options.httpLoader != nil {
|
|
r, er := l.options.httpLoader.Load(ctx)
|
|
if er != nil {
|
|
l.logger.Warnf("http loader: %v", er)
|
|
}
|
|
if v, _ := l.parsePatterns(r); v != nil {
|
|
patterns = append(patterns, v...)
|
|
}
|
|
}
|
|
|
|
l.logger.Debugf("load items %d", len(patterns))
|
|
return
|
|
}
|
|
|
|
func (l *connLimiter) parsePatterns(r io.Reader) (patterns []string, err error) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
if line := l.parseLine(scanner.Text()); line != "" {
|
|
patterns = append(patterns, line)
|
|
}
|
|
}
|
|
|
|
err = scanner.Err()
|
|
return
|
|
}
|
|
|
|
func (l *connLimiter) parseLine(s string) string {
|
|
if n := strings.IndexByte(s, '#'); n >= 0 {
|
|
s = s[:n]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
func (l *connLimiter) parseLimit(s string) (key string, limit int) {
|
|
s = strings.Replace(s, "\t", " ", -1)
|
|
s = strings.TrimSpace(s)
|
|
var ss []string
|
|
for _, v := range strings.Split(s, " ") {
|
|
if v != "" {
|
|
ss = append(ss, v)
|
|
}
|
|
}
|
|
if len(ss) < 2 {
|
|
return
|
|
}
|
|
|
|
key = ss[0]
|
|
limit, _ = strconv.Atoi(ss[1])
|
|
|
|
return
|
|
}
|
|
|
|
func (l *connLimiter) Close() error {
|
|
l.cancelFunc()
|
|
if l.options.fileLoader != nil {
|
|
l.options.fileLoader.Close()
|
|
}
|
|
if l.options.redisLoader != nil {
|
|
l.options.redisLoader.Close()
|
|
}
|
|
if l.options.httpLoader != nil {
|
|
l.options.httpLoader.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type cidrLimitEntry struct {
|
|
ipNet net.IPNet
|
|
limit ConnLimitGenerator
|
|
}
|
|
|
|
func (p *cidrLimitEntry) Network() net.IPNet {
|
|
return p.ipNet
|
|
}
|