2026-04-04 10:39:59 +00:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-04 13:07:29 +00:00
|
|
|
// NewRateLimiter creates a per-IP limiter, for example:
|
2026-04-04 10:39:59 +00:00
|
|
|
//
|
|
|
|
|
// rl := proxy.NewRateLimiter(cfg.RateLimit)
|
|
|
|
|
func NewRateLimiter(config RateLimit) *RateLimiter {
|
|
|
|
|
return &RateLimiter{
|
2026-04-04 14:16:33 +00:00
|
|
|
config: config,
|
2026-04-04 10:39:59 +00:00
|
|
|
buckets: make(map[string]*tokenBucket),
|
|
|
|
|
banned: make(map[string]time.Time),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:07:29 +00:00
|
|
|
// SetConfig swaps in a live reload value such as `proxy.RateLimit{MaxConnectionsPerMinute: 30}`.
|
2026-04-04 10:52:30 +00:00
|
|
|
//
|
|
|
|
|
// rl.SetConfig(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
|
2026-04-04 12:01:40 +00:00
|
|
|
func (rateLimiter *RateLimiter) SetConfig(config RateLimit) {
|
|
|
|
|
if rateLimiter == nil {
|
2026-04-04 10:52:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.mu.Lock()
|
2026-04-04 14:16:33 +00:00
|
|
|
rateLimiter.config = config
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.mu.Unlock()
|
2026-04-04 10:52:30 +00:00
|
|
|
}
|
|
|
|
|
|
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() }
|
2026-04-04 12:01:40 +00:00
|
|
|
func (rateLimiter *RateLimiter) Allow(ip string) bool {
|
|
|
|
|
if rateLimiter == nil {
|
2026-04-04 10:39:59 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host := remoteHost(ip)
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.mu.Lock()
|
|
|
|
|
defer rateLimiter.mu.Unlock()
|
2026-04-04 10:39:59 +00:00
|
|
|
|
2026-04-04 14:16:33 +00:00
|
|
|
if rateLimiter.config.MaxConnectionsPerMinute <= 0 {
|
2026-04-04 10:52:30 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
if bannedUntil, exists := rateLimiter.banned[host]; exists {
|
2026-04-04 10:39:59 +00:00
|
|
|
if bannedUntil.After(now) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-04-04 12:01:40 +00:00
|
|
|
delete(rateLimiter.banned, host)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
bucket, exists := rateLimiter.buckets[host]
|
2026-04-04 10:39:59 +00:00
|
|
|
if !exists {
|
|
|
|
|
bucket = &tokenBucket{
|
2026-04-04 14:16:33 +00:00
|
|
|
tokens: rateLimiter.config.MaxConnectionsPerMinute,
|
2026-04-04 10:39:59 +00:00
|
|
|
lastRefill: now,
|
|
|
|
|
}
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.buckets[host] = bucket
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.refillBucket(bucket, now)
|
2026-04-04 10:39:59 +00:00
|
|
|
if bucket.tokens <= 0 {
|
2026-04-04 14:16:33 +00:00
|
|
|
if rateLimiter.config.BanDurationSeconds > 0 {
|
|
|
|
|
rateLimiter.banned[host] = now.Add(time.Duration(rateLimiter.config.BanDurationSeconds) * time.Second)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket.tokens--
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tick removes expired ban entries and refills all token buckets. Called every second.
|
|
|
|
|
//
|
|
|
|
|
// rl.Tick()
|
2026-04-04 12:01:40 +00:00
|
|
|
func (rateLimiter *RateLimiter) Tick() {
|
|
|
|
|
if rateLimiter == nil {
|
2026-04-04 10:39:59 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now().UTC()
|
2026-04-04 12:01:40 +00:00
|
|
|
rateLimiter.mu.Lock()
|
|
|
|
|
defer rateLimiter.mu.Unlock()
|
2026-04-04 10:39:59 +00:00
|
|
|
|
2026-04-04 14:16:33 +00:00
|
|
|
if rateLimiter.config.MaxConnectionsPerMinute <= 0 {
|
2026-04-04 10:52:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
for host, bannedUntil := range rateLimiter.banned {
|
2026-04-04 10:39:59 +00:00
|
|
|
if !bannedUntil.After(now) {
|
2026-04-04 12:01:40 +00:00
|
|
|
delete(rateLimiter.banned, host)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
for _, bucket := range rateLimiter.buckets {
|
|
|
|
|
rateLimiter.refillBucket(bucket, now)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
func (rateLimiter *RateLimiter) refillBucket(bucket *tokenBucket, now time.Time) {
|
2026-04-04 14:16:33 +00:00
|
|
|
if bucket == nil || rateLimiter.config.MaxConnectionsPerMinute <= 0 {
|
2026-04-04 10:39:59 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 14:16:33 +00:00
|
|
|
refillEvery := time.Minute / time.Duration(rateLimiter.config.MaxConnectionsPerMinute)
|
2026-04-04 10:39:59 +00:00
|
|
|
if refillEvery <= 0 {
|
|
|
|
|
refillEvery = time.Second
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
elapsed := now.Sub(bucket.lastRefill)
|
|
|
|
|
if elapsed < refillEvery {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokensToAdd := int(elapsed / refillEvery)
|
|
|
|
|
bucket.tokens += tokensToAdd
|
2026-04-04 14:16:33 +00:00
|
|
|
if bucket.tokens > rateLimiter.config.MaxConnectionsPerMinute {
|
|
|
|
|
bucket.tokens = rateLimiter.config.MaxConnectionsPerMinute
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2026-04-04 14:16:33 +00:00
|
|
|
return &CustomDiff{defaultDifficulty: globalDiff}
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 10:52:30 +00:00
|
|
|
// SetGlobalDiff updates the default custom difficulty override.
|
|
|
|
|
//
|
|
|
|
|
// cd.SetGlobalDiff(100000)
|
2026-04-04 12:01:40 +00:00
|
|
|
func (customDiff *CustomDiff) SetGlobalDiff(globalDiff uint64) {
|
|
|
|
|
if customDiff == nil {
|
2026-04-04 10:52:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
customDiff.mu.Lock()
|
2026-04-04 14:16:33 +00:00
|
|
|
customDiff.defaultDifficulty = globalDiff
|
2026-04-04 12:01:40 +00:00
|
|
|
customDiff.mu.Unlock()
|
2026-04-04 10:52:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:07:29 +00:00
|
|
|
// OnLogin parses `WALLET+50000` into `WALLET` and `50000`.
|
2026-04-04 10:39:59 +00:00
|
|
|
//
|
|
|
|
|
// cd.OnLogin(proxy.Event{Miner: miner})
|
2026-04-04 12:01:40 +00:00
|
|
|
func (customDiff *CustomDiff) OnLogin(event Event) {
|
2026-04-04 10:39:59 +00:00
|
|
|
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 13:24:17 +00:00
|
|
|
event.Miner.SetCustomDiff(0)
|
2026-04-04 11:03:54 +00:00
|
|
|
return
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
if customDiff == nil {
|
2026-04-04 13:24:17 +00:00
|
|
|
event.Miner.SetCustomDiff(0)
|
2026-04-04 10:52:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 12:01:40 +00:00
|
|
|
customDiff.mu.RLock()
|
2026-04-04 14:16:33 +00:00
|
|
|
globalDiff := customDiff.defaultDifficulty
|
2026-04-04 12:01:40 +00:00
|
|
|
customDiff.mu.RUnlock()
|
2026-04-04 10:52:30 +00:00
|
|
|
if globalDiff > 0 {
|
|
|
|
|
event.Miner.SetCustomDiff(globalDiff)
|
2026-04-04 13:24:17 +00:00
|
|
|
return
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|
2026-04-04 13:24:17 +00:00
|
|
|
|
|
|
|
|
event.Miner.SetCustomDiff(0)
|
2026-04-04 10:39:59 +00:00
|
|
|
}
|