Add Blossom bandwidth limiting and tune rate limiters (v0.49.0)
Some checks failed
Go / build-and-release (push) Has been cancelled

- Add token-bucket bandwidth rate limiting for Blossom uploads
  - ORLY_BLOSSOM_RATE_LIMIT enables limiting (default: false)
  - ORLY_BLOSSOM_DAILY_LIMIT_MB sets daily limit (default: 10MB)
  - ORLY_BLOSSOM_BURST_LIMIT_MB sets burst cap (default: 50MB)
  - Followed users, admins, owners are exempt (unlimited)
- Change emergency mode throttling from exponential to linear scaling
  - Old: 16x multiplier at emergency threshold entry
  - New: 1x at threshold, +1x per 20% excess pressure
- Reduce follows ACL throttle increment from 200ms to 25ms per event
- Update dependencies

Files modified:
- app/blossom.go: Pass rate limit config to blossom server
- app/config/config.go: Add Blossom rate limit config options
- pkg/blossom/ratelimit.go: New bandwidth limiter implementation
- pkg/blossom/server.go: Add rate limiter integration
- pkg/blossom/handlers.go: Check rate limits on upload/mirror/media
- pkg/ratelimit/limiter.go: Linear emergency throttling
- pkg/acl/follows.go: Reduce default throttle increment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
woikos
2026-01-14 08:09:39 +01:00
parent 205f23fc0c
commit 9da1784b1b
17 changed files with 321 additions and 44 deletions

View File

@@ -19,6 +19,9 @@ type Server struct {
maxBlobSize int64
allowedMimeTypes map[string]bool
requireAuth bool
// Rate limiting for uploads
bandwidthLimiter *BandwidthLimiter
}
// Config holds configuration for the Blossom server
@@ -27,6 +30,11 @@ type Config struct {
MaxBlobSize int64
AllowedMimeTypes []string
RequireAuth bool
// Rate limiting (for non-followed users)
RateLimitEnabled bool
DailyLimitMB int64
BurstLimitMB int64
}
// NewServer creates a new Blossom server instance
@@ -48,6 +56,20 @@ func NewServer(db *database.D, aclRegistry *acl.S, cfg *Config) *Server {
}
}
// Initialize bandwidth limiter if enabled
var bwLimiter *BandwidthLimiter
if cfg.RateLimitEnabled {
dailyMB := cfg.DailyLimitMB
if dailyMB <= 0 {
dailyMB = 10 // 10MB default
}
burstMB := cfg.BurstLimitMB
if burstMB <= 0 {
burstMB = 50 // 50MB default burst
}
bwLimiter = NewBandwidthLimiter(dailyMB, burstMB)
}
return &Server{
db: db,
storage: storage,
@@ -56,6 +78,7 @@ func NewServer(db *database.D, aclRegistry *acl.S, cfg *Config) *Server {
maxBlobSize: cfg.MaxBlobSize,
allowedMimeTypes: allowedMap,
requireAuth: cfg.RequireAuth,
bandwidthLimiter: bwLimiter,
}
}
@@ -208,6 +231,44 @@ func (s *Server) checkACL(
return actual >= required
}
// isRateLimitExempt returns true if the user is exempt from rate limiting.
// Users with write access or higher (followed users, admins, owners) are exempt.
func (s *Server) isRateLimitExempt(pubkey []byte, remoteAddr string) bool {
if s.acl == nil {
return true // No ACL configured, no rate limiting
}
level := s.acl.GetAccessLevel(pubkey, remoteAddr)
// Followed users get "write" level, admins/owners get higher
// Only "read" and "none" are rate limited
return level == "write" || level == "admin" || level == "owner"
}
// checkBandwidthLimit checks if the upload is allowed under rate limits.
// Returns true if allowed, false if rate limited.
// Exempt users (followed, admin, owner) always return true.
func (s *Server) checkBandwidthLimit(pubkey []byte, remoteAddr string, sizeBytes int64) bool {
if s.bandwidthLimiter == nil {
return true // No rate limiting configured
}
// Check if user is exempt
if s.isRateLimitExempt(pubkey, remoteAddr) {
return true
}
// Use pubkey hex if available, otherwise IP
var identity string
if len(pubkey) > 0 {
identity = string(pubkey) // Will be converted to hex in handler
} else {
identity = remoteAddr
}
return s.bandwidthLimiter.CheckAndConsume(identity, sizeBytes)
}
// BaseURLKey is the context key for the base URL (exported for use by app handler)
type BaseURLKey struct{}