Skip to content

Add connection limits and rate limiting for mobile deployment #11

@melvincarvalho

Description

@melvincarvalho

Problem

The relay currently has no connection limits or rate limiting. On mobile devices with limited resources, this can lead to:

  • Resource exhaustion: Too many concurrent WebSocket connections
  • Memory overflow: Each connection consumes memory for subscriber maps
  • Battery drain: Processing messages from unlimited clients
  • DoS vulnerability: Malicious clients can overwhelm the relay

Mobile-Specific Concerns

Mobile devices running fonstr in Termux have:

  • Limited RAM: Typically 2-8GB shared with other apps
  • CPU constraints: ARM processors with thermal throttling
  • Battery limitations: Excessive processing drains battery quickly
  • Network limits: Mobile data connections may be metered

Proposed Solutions

Option 1: Basic connection limits

const MAX_CONNECTIONS = parseInt(process.env.MAX_CONNECTIONS) || 50
const connections = new Set()

fastify.register(async function (fastify) {
  fastify.get('/', { websocket: true }, async (con, req) => {
    // Check connection limit
    if (connections.size >= MAX_CONNECTIONS) {
      con.socket.close(1013, 'Too many connections')
      return
    }
    
    connections.add(con.socket)
    
    con.socket.on('close', () => {
      connections.delete(con.socket)
      subscribers.delete(con.socket)
    })
  })
})

Option 2: Per-IP connection limits

const connectionsByIP = new Map()
const MAX_CONNECTIONS_PER_IP = parseInt(process.env.MAX_CONNECTIONS_PER_IP) || 5

// In websocket handler
const clientIP = req.ip || req.socket.remoteAddress
const ipConnections = connectionsByIP.get(clientIP) || 0

if (ipConnections >= MAX_CONNECTIONS_PER_IP) {
  con.socket.close(1013, 'Too many connections from IP')
  return
}

Option 3: Rate limiting

const rateLimits = new Map() // IP -> { count, resetTime }
const RATE_LIMIT_WINDOW = 60000 // 1 minute
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX) || 100

function checkRateLimit(ip) {
  const now = Date.now()
  const limit = rateLimits.get(ip)
  
  if (!limit || now > limit.resetTime) {
    rateLimits.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW })
    return true
  }
  
  if (limit.count >= RATE_LIMIT_MAX) {
    return false
  }
  
  limit.count++
  return true
}

Option 4: Resource-based adaptive limits

function getAdaptiveLimit() {
  const memUsage = process.memoryUsage()
  const heapUsed = memUsage.heapUsed / 1024 / 1024 // MB
  
  // Reduce connection limit if memory usage is high
  if (heapUsed > 100) return Math.max(10, MAX_CONNECTIONS * 0.5)
  if (heapUsed > 50) return Math.max(25, MAX_CONNECTIONS * 0.75)
  return MAX_CONNECTIONS
}

Implementation Strategy

Phase 1: Basic limits

  • Global connection limit (default: 50)
  • Per-IP connection limit (default: 5)
  • Clean up disconnected sockets

Phase 2: Rate limiting

  • Message rate limiting per IP
  • Event submission rate limiting
  • REQ query rate limiting

Phase 3: Adaptive limits

  • Memory-based connection adjustment
  • CPU usage monitoring
  • Battery level consideration (if available)

Configuration Options

# Environment variables
MAX_CONNECTIONS=50           # Total connections
MAX_CONNECTIONS_PER_IP=5     # Per IP limit
RATE_LIMIT_MAX=100          # Messages per minute per IP
RATE_LIMIT_WINDOW=60000     # Rate limit window (ms)
ENABLE_RATE_LIMITING=true   # Enable rate limiting

Mobile-Optimized Defaults

const MOBILE_DEFAULTS = {
  MAX_CONNECTIONS: 25,
  MAX_CONNECTIONS_PER_IP: 3,
  RATE_LIMIT_MAX: 50,
  ENABLE_ADAPTIVE_LIMITS: true
}

Benefits for Mobile

  • Resource protection: Prevents memory/CPU exhaustion
  • Battery preservation: Limits processing overhead
  • Stability: Prevents relay crashes from resource exhaustion
  • DoS protection: Basic protection against abuse
  • Predictable performance: Consistent response times

Implementation Considerations

Memory management:

  • Clean up tracking maps when connections close
  • Periodic cleanup of stale rate limit entries
  • Monitor memory usage for adaptive limits

Backwards compatibility:

  • Limits disabled by default (for now)
  • Graceful connection rejection
  • Informative close codes/messages

Mobile detection:

const isMobile = process.env.MOBILE_MODE === 'true' || 
                 process.platform === 'android' ||
                 process.env.TERMUX_VERSION !== undefined

Error Handling

  • WebSocket close codes for different rejection reasons
  • Clear error messages in NOTICE events
  • Logging of connection rejections (if logging enabled)

Testing Requirements

  1. Verify connection limits work correctly
  2. Test rate limiting accuracy
  3. Validate memory cleanup on disconnect
  4. Test adaptive limits under load
  5. Ensure no memory leaks from tracking maps

This feature would make fonstr much more robust for mobile deployment and protect against resource exhaustion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions