2026-04-04 10:39:59 +00:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// NewRateLimiter allocates a per-IP token bucket limiter.
|
|
|
|
|
//
|
|
|
|
|
// rl := proxy.NewRateLimiter(cfg.RateLimit)
|
|
|
|
|
func NewRateLimiter(config RateLimit) *RateLimiter {
|
|
|
|
|
return &RateLimiter{
|
|
|
|
|
cfg: config,
|
|
|
|
|
buckets: make(map[string]*tokenBucket),
|
|
|
|
|
banned: make(map[string]time.Time),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
// SetConfig replaces the active rate-limit settings.
|
|
|
|
|
//
|
|
|
|
|
// rl.SetConfig(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
|
|
|
|
|
func (rl *RateLimiter) SetConfig(config RateLimit) {
|
|
|
|
|
if rl == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
rl.cfg = config
|
|
|
|
|
rl.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:39:59 +00:00
|
|
|
// Allow returns true if the IP address is permitted to open a new connection. Thread-safe.
|
|
|
|
|
//
|
|
|
|
|
// if rl.Allow(conn.RemoteAddr().String()) { proceed() }
|
|
|
|
|
func (rl *RateLimiter) Allow(ip string) bool {
|
2026-04-04 10:52:30 +00:00
|
|
|
if rl == nil {
|
2026-04-04 10:39:59 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host := remoteHost(ip)
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
defer rl.mu.Unlock()
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
if rl.cfg.MaxConnectionsPerMinute <= 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:39:59 +00:00
|
|
|
if bannedUntil, exists := rl.banned[host]; exists {
|
|
|
|
|
if bannedUntil.After(now) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
delete(rl.banned, host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket, exists := rl.buckets[host]
|
|
|
|
|
if !exists {
|
|
|
|
|
bucket = &tokenBucket{
|
|
|
|
|
tokens: rl.cfg.MaxConnectionsPerMinute,
|
|
|
|
|
lastRefill: now,
|
|
|
|
|
}
|
|
|
|
|
rl.buckets[host] = bucket
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rl.refillBucket(bucket, now)
|
|
|
|
|
if bucket.tokens <= 0 {
|
|
|
|
|
if rl.cfg.BanDurationSeconds > 0 {
|
|
|
|
|
rl.banned[host] = now.Add(time.Duration(rl.cfg.BanDurationSeconds) * time.Second)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket.tokens--
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tick removes expired ban entries and refills all token buckets. Called every second.
|
|
|
|
|
//
|
|
|
|
|
// rl.Tick()
|
|
|
|
|
func (rl *RateLimiter) Tick() {
|
2026-04-04 10:52:30 +00:00
|
|
|
if rl == nil {
|
2026-04-04 10:39:59 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
defer rl.mu.Unlock()
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
if rl.cfg.MaxConnectionsPerMinute <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:39:59 +00:00
|
|
|
for host, bannedUntil := range rl.banned {
|
|
|
|
|
if !bannedUntil.After(now) {
|
|
|
|
|
delete(rl.banned, host)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, bucket := range rl.buckets {
|
|
|
|
|
rl.refillBucket(bucket, now)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (rl *RateLimiter) refillBucket(bucket *tokenBucket, now time.Time) {
|
|
|
|
|
if bucket == nil || rl.cfg.MaxConnectionsPerMinute <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refillEvery := time.Minute / time.Duration(rl.cfg.MaxConnectionsPerMinute)
|
|
|
|
|
if refillEvery <= 0 {
|
|
|
|
|
refillEvery = time.Second
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
elapsed := now.Sub(bucket.lastRefill)
|
|
|
|
|
if elapsed < refillEvery {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokensToAdd := int(elapsed / refillEvery)
|
|
|
|
|
bucket.tokens += tokensToAdd
|
|
|
|
|
if bucket.tokens > rl.cfg.MaxConnectionsPerMinute {
|
|
|
|
|
bucket.tokens = rl.cfg.MaxConnectionsPerMinute
|
|
|
|
|
}
|
|
|
|
|
bucket.lastRefill = bucket.lastRefill.Add(time.Duration(tokensToAdd) * refillEvery)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewCustomDiff stores the default custom difficulty override.
|
|
|
|
|
//
|
|
|
|
|
// cd := proxy.NewCustomDiff(50000)
|
|
|
|
|
func NewCustomDiff(globalDiff uint64) *CustomDiff {
|
|
|
|
|
return &CustomDiff{globalDiff: globalDiff}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
// SetGlobalDiff updates the default custom difficulty override.
|
|
|
|
|
//
|
|
|
|
|
// cd.SetGlobalDiff(100000)
|
|
|
|
|
func (cd *CustomDiff) SetGlobalDiff(globalDiff uint64) {
|
|
|
|
|
if cd == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cd.mu.Lock()
|
|
|
|
|
cd.globalDiff = globalDiff
|
|
|
|
|
cd.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:39:59 +00:00
|
|
|
// OnLogin parses miner.User for a "+{number}" suffix and sets miner.CustomDiff.
|
|
|
|
|
//
|
|
|
|
|
// cd.OnLogin(proxy.Event{Miner: miner})
|
|
|
|
|
func (cd *CustomDiff) OnLogin(event Event) {
|
|
|
|
|
if event.Miner == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := event.Miner.User()
|
|
|
|
|
index := strings.LastIndex(user, "+")
|
2026-04-04 11:03:54 +00:00
|
|
|
if index > 0 && index < len(user)-1 {
|
2026-04-04 10:39:59 +00:00
|
|
|
if value, errorValue := strconv.ParseUint(user[index+1:], 10, 64); errorValue == nil {
|
|
|
|
|
event.Miner.SetUser(user[:index])
|
|
|
|
|
event.Miner.SetCustomDiff(value)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-04 11:03:54 +00:00
|
|
|
return
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
if cd == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cd.mu.RLock()
|
|
|
|
|
globalDiff := cd.globalDiff
|
|
|
|
|
cd.mu.RUnlock()
|
|
|
|
|
if globalDiff > 0 {
|
|
|
|
|
event.Miner.SetCustomDiff(globalDiff)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
}
|