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
- 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) } - 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) } - 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
- 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 } - 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 } - 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
- 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 } - 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 } - 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 } }