Security and Anti-Cheat
Server-authoritative validation, movement checks, teleport detection, hit validation, rate limiting, and transport encryption in Aether.
Aether uses server-authoritative validation to prevent cheating in competitive and social VR worlds. The aether-security crate provides movement validation, teleport detection, hit registration verification, action rate limiting, interest management, and transport hardening -- all as pure functions and lightweight structs with no external I/O.
Key Concepts
- Server authority -- The server is the single source of truth for game state. Client inputs are validated before being applied.
- Movement validation -- Per-tick velocity and acceleration checks against physics limits.
- Teleport detection -- Detects impossible position jumps between server ticks.
- Hit validation -- Server-side verification of hit claims (range, line-of-sight, timing).
- Action rate limiting -- Token-bucket rate limiter with per-action cooldowns.
- Interest management -- Controls which entities are visible to which players.
- Transport security -- QUIC/TLS encryption and DDoS defense signals.
Architecture
All validators are composable and run in sequence on the server for every client input packet:
Client Input Packet
|
v
Server Input Router
|
+----+--------+----------+----------+
| | | | |
v v v v v
Move Teleport Hit Rate Limit WASM
Valid Detect Valid Check Sandbox
| | | | |
+----+--------+----------+----------+
|
v
Accept / Reject (CheatVerdict)
|
[if accepted]
v
Apply to Server State
|
v
Interest Manager -> Filter Entity Updates -> Send to Client
Each validator returns a typed result (accept or reject with reason), making the pipeline testable and auditable.
Movement Validation
The movement validator checks that player position changes respect physics constraints:
use aether_security::{MovementValidator, MovementConfig, ValidationResult};
let config = MovementConfig {
max_speed: 20.0, // meters per second
max_acceleration: 50.0, // meters per second squared
tolerance: 1.1, // 10% tolerance for network jitter
};
let result = MovementValidator::validate(
prev_position, // [f32; 3]
new_position, // [f32; 3]
delta_time, // seconds since last tick
&config,
);
match result {
ValidationResult::Accepted => {
// Apply the new position to server state
}
ValidationResult::Rejected { reason } => {
// Log the cheat signal, use server-predicted position instead
}
}
The validator computes:
- Velocity = distance / delta_time. Rejected if > max_speed * tolerance.
- Acceleration = velocity delta / delta_time. Rejected if > max_acceleration * tolerance.
Teleport Detection
The teleport detector is stateful, tracking each entity's last known position:
use aether_security::{TeleportDetector, TeleportResult};
let mut detector = TeleportDetector::new(100.0); // max 100m displacement per tick
// First check establishes baseline
detector.check(entity_id, position_a, timestamp_a);
// Subsequent checks flag impossible jumps
match detector.check(entity_id, position_b, timestamp_b) {
TeleportResult::Normal => {
// Movement is within physically possible bounds
}
TeleportResult::Teleported { distance } => {
// Impossible displacement detected
// distance: actual displacement between ticks
}
}
The detector flags movements where the distance between consecutive positions exceeds the physically possible displacement given the time elapsed and maximum speed.
Hit Validation
The hit validator verifies client-submitted hit claims against server-authoritative state:
use aether_security::{HitValidator, HitClaim, HitResult};
let claim = HitClaim {
attacker_id: attacker,
target_id: target,
weapon_range: 10.0,
timestamp: hit_time,
attacker_position: [1.0, 0.0, 0.0],
target_position: [5.0, 0.0, 0.0],
};
match HitValidator::validate(&claim, &server_state) {
HitResult::Valid => {
// Apply damage to target
}
HitResult::Invalid { reason } => {
// Reject the hit claim
}
}
Validation checks:
| Check | Description |
|---|---|
| Range | Distance between attacker and target must be within weapon range |
| Line of sight | Plausibility check that the target was visible to the attacker |
| Timing window | Hit timestamp must be within an acceptable window of the server tick |
| Target alive | Target must be in a damageable state on the server |
Action Rate Limiting
The action rate limiter uses a token-bucket algorithm per (player, action) pair:
use aether_security::{ActionRateLimiter, RateLimitResult};
let mut limiter = ActionRateLimiter::new();
// Configure per action type
limiter.configure_action("attack", 2.0, 10); // 2 tokens/sec, burst of 10
limiter.configure_action("jump", 1.0, 3); // 1 token/sec, burst of 3
match limiter.try_consume(player_id, "attack", now) {
RateLimitResult::Allowed { remaining } => {
// Action permitted
}
RateLimitResult::Limited { retry_after } => {
// Action rejected, try again after `retry_after` duration
}
}
Each action type has independent token generation rates and burst capacities. This prevents action flooding (spamming attacks, jumps, or interactions faster than the game design allows).
Interest Management
Interest management controls which entities are visible to which players, preventing information leaks:
use aether_security::{InterestManager, InterestZone};
let mut manager = InterestManager::new(200.0); // 200m default interest radius
// Add spatial interest zones with access control
manager.add_zone(InterestZone {
center: [100.0, 0.0, 100.0],
radius: 50.0,
allowed_players: vec![player_a, player_b],
});
// Query visible entities for a player
let visible = manager.visible_entities(
player_id,
player_position,
&all_entities,
);
// Only entities within interest radius and with appropriate access are returned
Culling rules:
- Entities outside the player's interest radius are not sent to the client.
- Hidden entities (invisible players, GM-cloaked entities) are filtered unless the viewer has explicit permission.
- Interest zones can restrict visibility to specific player lists.
WASM Sandbox Security
Scripts running in the WASM sandbox are constrained by capability limits (WasmSandboxCapability). Scripts requesting unauthorized capabilities are rejected, and module tampering (hash mismatch) is detected via WasmSurfaceError::ModuleTampered.
Transport Security
The gateway enforces encrypted QUIC/TLS transport for all connections via TransportPolicy. DDoS defense uses a state machine (Normal -> Alert -> Mitigation -> Recovery) triggered by AttackSignal variants like volumetric floods.
Default Constants
All default limits are defined as constants and overridable via environment variables:
| Constant | Default | Description |
|---|---|---|
DEFAULT_MAX_SPEED | 20.0 m/s | Maximum player movement speed |
DEFAULT_MAX_ACCELERATION | 50.0 m/s^2 | Maximum acceleration |
DEFAULT_SPEED_TOLERANCE | 1.1 | 10% tolerance for network jitter |
DEFAULT_MAX_TELEPORT_DISTANCE | 100.0 m | Max displacement per tick |
DEFAULT_INTEREST_RADIUS | 200.0 m | Default entity visibility radius |
DEFAULT_MAX_HIT_RANGE | 100.0 m | Maximum weapon hit range |
DEFAULT_HIT_TIMING_WINDOW_MS | 500 | Acceptable hit timing window |
DEFAULT_RATE_LIMIT_BURST | 10 | Default token bucket burst |
Cheat Verdicts
When a validator rejects an input, it produces a CheatVerdict with structured information:
use aether_security::{CheatVerdict, CheatSignal};
// Verdicts carry the signal type and severity
let verdict = CheatVerdict {
player_id: cheater,
signal: CheatSignal::SpeedHack { velocity: 45.0, max_allowed: 22.0 },
severity: 0.8,
timestamp: now,
};
Verdicts are collected and forwarded to the trust and safety system for action (warnings, kicks, or bans) based on accumulated severity scores.