Server-Authoritative Architecture

The fundamental principle is never trust the client. The server must be the single source of truth for all game state.

Server Authority Principles:

  • Input-Only Clients: Clients send input (WASD, mouse clicks), server calculates results
  • State Ownership: All critical game state lives on server
  • Validation First: Every client request must be validated before execution
// Good: Client sends input, server decides outcome
type PlayerInput struct {
    PlayerID   uint32 `json:"player_id"`
    Action     string `json:"action"`     // "move", "attack", etc.
    Direction  string `json:"direction"`  // "north", "south", etc.
    Timestamp  int64  `json:"timestamp"`
}
 
// Bad: Client sends direct state changes
type PlayerState struct {
    PlayerID  uint32  `json:"player_id"`
    Position  Vector3 `json:"position"`  // Never trust client positions!
    Health    int     `json:"health"`    // Never trust client health!
}

Packet Structure & Validation

type GamePacket struct {
    PacketID   uint8   `json:"packet_id"`    // Action type (8 bits = 256 types)
    PlayerID   uint32  `json:"player_id"`    // Who sent this
    Timestamp  int64   `json:"timestamp"`    // When sent
    Data       []byte  `json:"data"`         // Action-specific data
    Checksum   uint32  `json:"checksum"`     // Integrity check
}
 
// Specific action packets
type MovePacket struct {
    Direction  uint8   `json:"direction"`    // 0-7 for 8 directions
    Speed      uint8   `json:"speed"`        // 0=walk, 1=run
}
 
type AttackPacket struct {
    TargetType uint8   `json:"target_type"`  // 0=player, 1=npc, 2=object  
    TargetID   uint32  `json:"target_id"`    // ID of target
    WeaponSlot uint8   `json:"weapon_slot"`  // Which weapon (0-9)
}

Critical Validation Patterns

  1. Ownership Validation
    func (s *GameServer) HandleUseItem(packet *UseItemPacket, playerID uint32) error {
        // CRITICAL: Verify player owns the item
        if !s.PlayerOwnsItem(playerID, packet.ItemID) {
            return errors.New("player does not own item")
        }
     
        // CRITICAL: Verify item is in player's inventory, not vendor/ground/etc
        if !s.IsItemInPlayerInventory(playerID, packet.ItemID) {
            return errors.New("item not in player inventory")
        }
        return s.UseItem(playerID, packet.ItemID)
    }
  2. Scope Limitation
    // Good: Use inventory slot indices (limits scope)
    type DismantlePacket struct {
        InventorySlot uint8 `json:"slot"` // 0-39 for 40 slot inventory
    }
     
    // Bad: Use global item IDs (can reference any item in game)
    type BadDismantlePacket struct {
        ItemID uint64 `json:"item_id"` // Can reference ANY item!
    }
     
    func (s *GameServer) HandleDismantle(packet *DismantlePacket, playerID uint32) error {
        if packet.InventorySlot >= MaxInventorySlots {
            return errors.New("invalid inventory slot")
        }
        
        item := s.GetPlayerInventorySlot(playerID, packet.InventorySlot)
        if item == nil {
            return errors.New("no item in slot")
        }
        
        return s.DismantleItem(playerID, item)
    }
  3. Player ID Validation
    func (s *GameServer) HandleSitDown(packet *SitPacket, connectionPlayerID uint32) error {
        // CRITICAL: Ignore packet PlayerID, use connection's authenticated ID
        if packet.PlayerID != connectionPlayerID {
            return errors.New("player ID mismatch")
        }
        
        // Better: Remove PlayerID from packet entirely
        return s.SitPlayer(connectionPlayerID)
    }
     
    // Even better packet design - no PlayerID field needed
    type SitPacket struct {
        // PlayerID determined from authenticated connection
        Duration uint16 `json:"duration"` // Optional sit duration
    }

Common Exploit Patterns & Fixes

  1. Resource Duplication
    // Vulnerable: Item destroyed after success check
    func (s *GameServer) HandleUseScroll(packet *UseScrollPacket) error {
        s.UseItem(packet.ScrollID) // Item used but not destroyed yet!
        
        if s.IsScroll(packet.ScrollID) {
            s.IdentifyItem(packet.TargetID)
            s.DestroyItem(packet.ScrollID) // Only destroyed if it's a scroll
            return nil
        }
        // BUG: Non-scrolls are used but never destroyed!
        return errors.New("not a scroll")
    }
     
    // Fixed: Validate before use, destroy on any use
    func (s *GameServer) HandleUseScroll(packet *UseScrollPacket) error {
        if !s.IsScroll(packet.ScrollID) {
            return errors.New("not a scroll")
        }
        
        if err := s.IdentifyItem(packet.TargetID); err != nil {
            return err
        }
        
        s.DestroyItem(packet.ScrollID) // Always destroy on success
        return nil
    }
  2. Zero/Negative Values
    func (s *GameServer) ValidateTradeQuantity(itemID uint64, quantity int32) error {
        if quantity <= 0 {
            return errors.New("invalid quantity")
        }
        if quantity > s.GetItemStackSize(itemID) {
            return errors.New("quantity exceeds stack size")
        }
        return nil
    }
  3. Debug Fields Removal
    // Production packet - clean
    type AcceptQuestPacket struct {
        QuestID uint32 `json:"quest_id"`
    }
     
    // NEVER leave debug fields in production
    type BadAcceptQuestPacket struct {
        QuestID    uint32 `json:"quest_id"`
        DebugSkip  uint32 `json:"debug_skip"` // REMOVE THIS!
    }

Advanced Security Techniques

  1. Rate Limiting & Throttling
    type RateLimiter struct {
        requests map[uint32][]time.Time
        maxRate  int
        window   time.Duration
        mu       sync.RWMutex
    }
     
    func (rl *RateLimiter) AllowAction(playerID uint32) bool {
        rl.mu.Lock()
        defer rl.mu.Unlock()
        now := time.Now()
        // Clean old requests
        rl.requests[playerID] = lo.Filter(rl.requests[playerID], func(t time.Time, _ int) bool {
            return now.Sub(t) < rl.window
        })
        
        if len(rl.requests[playerID]) >= rl.maxRate {
            return false
        }
        
        rl.requests[playerID] = append(rl.requests[playerID], now)
        return true
    }
  2. State Reconciliation
    func (s *GameServer) HandlePlayerMove(packet *MovePacket, playerID uint32) error {
        // Server-side movement calculation
        newPosition := s.CalculateMovement(playerID, packet.Direction, packet.DeltaTime)
        
        // Validate movement is possible (collision, speed limits, etc.)
        if !s.IsValidMovement(playerID, newPosition) {
            // Send correction back to client
            s.SendPositionCorrection(playerID, s.GetPlayerPosition(playerID))
            return errors.New("invalid movement")
        }
        
        s.SetPlayerPosition(playerID, newPosition)
        s.BroadcastPlayerMove(playerID, newPosition)
        return nil
    }
  3. Anti-Cheat Monitoring
    type CheatDetector struct {
        suspiciousActions map[uint32]int
        threshold         int
    }
     
    func (cd *CheatDetector) RecordSuspiciousAction(playerID uint32, action string) {
        cd.suspiciousActions[playerID]++
        
        if cd.suspiciousActions[playerID] > cd.threshold {
            log.Printf("CHEAT ALERT: Player %d exceeded suspicion threshold", playerID)
            // Take action: kick, ban, flag for manual review
        }
    }