Eksploitasi sistem matchmaking sering terjadi akibat celah konkurensi (race condition) antara client dan backend worker. Masalah klasik yang pernah mencuat pada game kompetitif seperti seri FIFA adalah manipulasi sinyal pembatalan (cancel) atau disconnect saat server sedang memasangkan pemain. Ketika client mengirim request cancel tepat di milidetik yang sama saat matchmaker menetapkan lawan, sistem tanpa kontrol konkurensi ketat dapat memicu ghost match, pembatalan sepihak tanpa penalti, atau tiket yang tertinggal dalam status tidak konsisten.
Anatomi Celah: Race Condition Cancel vs Match Found
Pola serangan manipulasi antrian umumnya memanfaatkan asinkronisitas jaringan:
- Pemain A masuk antrian pencarian (status:
QUEUED). - Matchmaker backend menemukan pasangan untuk Pemain A dan Pemain B.
- Sebelum worker matchmaker mengunci tiket dan mengubah status menjadi
MATCHED, Pemain A secara sengaja mengirim perintah pembatalan antrian (HTTP POST/matchmaking/cancel) atau memutus soket TCP. - Jika endpoint pembatalan hanya membaca status dari cache lokal atau query DB non-transaksional, server memvalidasi tiket masih
QUEUEDlalu mengembalikan status sukses pembatalan. - Worker matchmaker menyelesaikan tugasnya dan menganggap Pemain A siap bertanding, sementara client Pemain A telah menutup koneksi. Pemain B terjebak dalam room tanpa lawan (ghost match).
Solusi untuk masalah ini adalah menjamin linearizability: transisi status tiket harus bersifat deterministik, terurut, dan atomik.
State Machine Tiket Matchmaking
Sistem antrian membutuhkan finite state machine (FSM) yang kaku. Transisi status yang diizinkan:
QUEUED→MATCHED(hanya diizinkan oleh Matchmaker Worker)QUEUED→CANCELLED(diizinkan oleh Client request)MATCHED→CONFIRMED(kedua client mengirim heartbeat/readiness)MATCHED→EXPIRED(salah satu client gagal confirm dalam batas TTL)
Kunci pengamanan: client dilarang membatalkan tiket jika status sudah berpindah ke MATCHED. Begitu pula worker tidak boleh memasangkan tiket yang sudah berstatus CANCELLED.
Atomic State Transition dengan Redis Lua Script
Pendekatan multi-step (GET tiket → validasi di aplikasi → SET tiket) di Redis rentan terhadap interleaving. Evaluasi kondisi dan mutasi state wajib dilakukan dalam satu atomic step menggunakan Lua script.
Simpan data tiket menggunakan Redis Hash dengan key ticket:{ticket_id} yang memiliki field status, player_id, dan idempotency_token.
-- KEYS[1]: Nama key tiket (contoh: ticket:abc-123)
-- ARGV[1]: Expected current status (contoh: QUEUED)
-- ARGV[2]: Target new status (contoh: MATCHED)
-- ARGV[3]: Idempotency token baru dari worker atau client
local current_status = redis.call('HGET', KEYS[1], 'status')
if not current_status then
return redis.error_reply('ERR_TICKET_NOT_FOUND')
end
if current_status ~= ARGV[1] then
-- Mengembalikan status terkini saat ini jika transisi gagal
return { 'REJECTED', current_status }
end
redis.call('HSET', KEYS[1], 'status', ARGV[2], 'idempotency_token', ARGV[3])
return { 'OK', ARGV[2] }
Script di atas menjamin worker matchmaker dan request cancel client saling mengunci secara implisit melalui single-threaded execution Redis. Jika cancel request tiba lebih dulu, status berubah menjadi CANCELLED; saat worker mencoba merubahnya ke MATCHED, Redis mengembalikan {'REJECTED', 'CANCELLED'} sehingga worker dapat mencari kandidat lain tanpa membuat match invalid.
Distributed Locking untuk Matchmaking Worker
Ketika matchmaking worker memproses sekelompok tiket secara batch, worker harus memegang kunci eksklusif atas ID pemain yang sedang diproses. Gunakan perintah standar Redis SET NX PX untuk mengunci resource:
// Pseudocode Worker Go/Node.js
func LockPlayer(redisClient, playerID, requestID string, ttlMs int) bool {
key := fmt.Sprintf("lock:player:%s", playerID)
// PX: milidetik expiry, NX: hanya set jika key belum ada
ok, err := redisClient.SetNX(ctx, key, requestID, ttlMs).Result()
return err == nil && ok
}
Pelepasan distributed lock juga wajib menggunakan Lua script untuk memastikan worker hanya menghapus lock miliknya sendiri (mencegah release lock yang sudah expired dan diambil alih worker lain):
-- KEYS[1]: Lock key (lock:player:12345)
-- ARGV[1]: Request ID / Owner token unik worker
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
Handling Timeout dan Ghost Match
Celah exploit disconnect sering terjadi saat pemain mematikan game tepat saat match dibuat untuk menghindari lawan yang berat. Terapkan strategi berikut:
- Two-Phase Confirmation: Saat status berubah menjadi
MATCHED, room server tidak langsung memulai game. Berikan TTL 10-15 detik pada Redis key match. Klien kedua belah pihak wajib mengirim packetACK(Confirm). - Cleanup Worker: Jika salah satu klien gagal mengirimkan konfirmasi sebelum TTL habis, transisikan state ke
EXPIRED. Pemain yang gagal confirm diberikan cooldown penalty (penalti antrian), sedangkan pemain yang lolos confirm dikembalikan ke antrian terdepan dengan prioritas tinggi.
Catatan Keamanan: Jangan percaya paket disconnect dari client. Setiap pembatalan wajib diverifikasi melalui server-side state machine.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!