Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vulnerability 6: Replay Tampering

The Problem

Modified replay files to fake tournament results.

Mitigation: Signed Hash Chain

#![allow(unused)]
fn main() {
pub struct SignedReplay {
    pub data: ReplayData,
    pub server_signature: Ed25519Signature,
    pub hash_chain: Vec<(u64, u64)>,  // tick, cumulative_hash
}

impl SignedReplay {
    pub fn verify(&self, server_public_key: &PublicKey) -> bool {
        // 1. Verify server signature
        // 2. Verify hash chain integrity (tampering any tick invalidates all subsequent)
    }
}
}

Vulnerability 7: Reconciler as Attack Surface

The Problem

If the client accepts “corrections” from an external authority (cross-engine reconciler), a fake server could send malicious corrections.

Mitigation: Bounded and Authenticated Corrections

#![allow(unused)]
fn main() {
fn is_sane_correction(&self, c: &EntityCorrection) -> bool {
    match &c.field {
        CorrectionField::Position(new_pos) => {
            let current = self.sim.entity_position(c.entity);
            let max_drift = MAX_UNIT_SPEED * self.ticks_since_sync;
            current.distance_to(new_pos) <= max_drift
        }
        CorrectionField::Credits(amount) => {
            *amount >= 0 &&
            (*amount - self.last_known_credits).abs() <= MAX_CREDIT_DELTA
        }
    }
}
}

All corrections must be: signed by the authority, bounded to physically possible values, and rejectable if suspicious.

Vulnerability 8: Join Code Brute-Forcing

The Problem

Join codes (e.g., IRON-7K3M) enable NAT-friendly direct joins to player-hosted relays via a rendezvous server. If codes are short, an attacker can brute-force codes to join games uninvited — griefing lobbies or extracting connection info.

A 4-character alphanumeric code has ~1.7 million combinations. At 1000 requests/second, exhausted in ~28 minutes. Shorter codes are worse.

Mitigation: Length + Rate Limiting + Expiry

#![allow(unused)]
fn main() {
pub struct JoinCode {
    pub code: String,          // 6-8 chars, alphanumeric, no ambiguous chars (0/O, 1/I/l)
    pub created_at: Instant,
    pub expires_at: Instant,   // TTL: 5 minutes (enough to share, too short to brute-force)
    pub uses_remaining: u32,   // 1 for private, N for party invites
}

impl RendezvousServer {
    fn resolve_code(&mut self, code: &str, requester_ip: IpAddr) -> Result<ConnectionInfo> {
        // Rate limit: max 5 resolve attempts per IP per minute
        if self.rate_limiter.check(requester_ip).is_err() {
            return Err(RateLimited);
        }
        // Lookup and consume
        match self.codes.get(code) {
            Some(entry) if entry.expires_at > Instant::now() => Ok(entry.connection_info()),
            _ => Err(InvalidCode),  // Don't distinguish "expired" from "nonexistent"
        }
    }
}
}

Key choices:

  • 6+ characters from a 32-char alphabet (no ambiguous chars) = ~1 billion combinations
  • Rate limit resolves per IP (5/minute blocks brute-force, legitimate users never hit it)
  • Codes expire after 5 minutes (limits attack window)
  • Invalid vs expired returns the same error (no information leakage)

Vulnerability 9: Tracking Server Abuse

The Problem

The tracking server is a public API. Abuse vectors:

  • Spam listings — flood with fake games, burying real ones
  • Phishing redirects — listing points to a malicious IP that mimics a game server but captures client info
  • DDoS — overwhelm the server to deny game discovery for everyone

OpenRA’s master server has been DDoSed before. Any public game directory faces this.

Mitigation: Standard API Hardening

#![allow(unused)]
fn main() {
pub struct TrackingServerConfig {
    pub max_listings_per_ip: u32,        // 3 — one IP rarely needs more
    pub heartbeat_interval: Duration,    // 30s — listing expires if missed
    pub listing_ttl: Duration,           // 2 minutes without heartbeat → removed
    pub browse_rate_limit: u32,          // 30 requests/minute per IP
    pub publish_rate_limit: u32,         // 5 requests/minute per IP
    pub require_valid_game_port: bool,   // Server verifies the listed port is reachable
}
}

Spam prevention: Limit listings per IP. Require heartbeats (real games send them, spam bots must sustain effort). Optionally verify the listed port actually responds to a game protocol handshake.

Phishing prevention: Client validates the game protocol handshake before showing the lobby. A non-game server at the listed IP fails handshake and is silently dropped from the browser.

DDoS: Standard infrastructure — CDN/reverse proxy for the browse API, rate limiting, geographic distribution. The tracking server is stateless and trivially horizontally scalable (it’s just a filtered list in memory).

Vulnerability 10: Client Version Mismatch

The Problem

Players with different client versions join the same game. Even minor differences in sim code (bug fix, balance patch) cause immediate desyncs. This looks like a bug to users, destroys trust, and wastes time. Age of Empires 2 DE had years of desync issues partly caused by version mismatches.

Mitigation: Version Handshake at Connection

#![allow(unused)]
fn main() {
pub struct VersionInfo {
    pub engine_version: SemVer,        // e.g., 0.3.1
    pub sim_hash: u64,                 // hash of compiled sim logic (catches patched binaries)
    pub mod_manifest_hash: u64,        // hash of loaded mod rules (catches different mod versions)
    pub protocol_version: u32,         // wire protocol version
}

impl GameLobby {
    fn accept_player(&self, remote: &VersionInfo) -> Result<()> {
        if remote.protocol_version != self.host.protocol_version {
            return Err(IncompatibleProtocol);
        }
        if remote.sim_hash != self.host.sim_hash {
            return Err(SimVersionMismatch);
        }
        if remote.mod_manifest_hash != self.host.mod_manifest_hash {
            return Err(ModMismatch);
        }
        Ok(())
    }
}
}

Key: Check version during lobby join, not after game starts. The relay server and tracking server listings both include VersionInfo so incompatible games are filtered from the browser entirely.