10. 델타 스냅샷 동기화 + 리플레이 방어 (종합)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 델타 스냅샷 동기화 + 리플레이 방어 (종합/최상)
// ----------------------------------------------------------------------------
// 시나리오(현실적):
// - 서버는 각 클라에 월드 상태를 "델타 스냅샷"으로 보낸다:
// 서버는 tick 마다 스냅샷을 만들고, 클라가 ack 한 baseline 스냅샷과의
// 차분(delta)만 전송한다. 클라는 baseline 에 델타를 적용해 현재 상태를 복원한다.
// 와이어: [uint snapTick][uint baseTick][delta...]
// - 반대 방향(클라→서버)의 명령 패킷은 리플레이/변조를 막아야 한다:
// 와이어: [ulong nonce][uint clientTimeMs][cmdPayload...][mac(생략)]
// 서버는 nonce 중복과 시간 창(window)으로 리플레이를 거른다.
// - UDP 기반이라 패킷은 순서뒤바뀜/유실/중복/지연되어 도착한다.
// - 클라는 신뢰 불가(치터/리플레이/프록시). 멀티스레드 서버다.
//
// 요구사항:
// - 델타는 클라가 "실제로 가진 baseline"에만 적용되어야 한다. 어긋나면 상태가 발산한다.
// - 같은 명령이 두 번 처리되면(리플레이) 경제/전투 사고가 난다.
// - 어떤 입력/타이밍에도 서버가 죽거나 무한히 메모리를 쓰면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생·악용되는지 입체적으로 설명하고,
// 수정안과 더 나은 설계(델타 동기화 안전성 / 리플레이 방어 / 버전·상태 합의)를 제시하라.
// ============================================================================
using System;
using System.Collections.Generic;
// ---------------------------------------------------------------------------
// 서버 → 클라: 델타 스냅샷 송신
// ---------------------------------------------------------------------------
public class Snapshot
{
public uint tick;
public byte[] state;
}
public class DeltaSender
{
// 클라가 마지막으로 ack 한 baseline tick
public uint ackedTick = 0;
// 최근 스냅샷 히스토리 (tick -> snapshot)
public Dictionary<uint, Snapshot> history = new Dictionary<uint, Snapshot>();
// 이번 tick 스냅샷을 만들고 델타를 보낸다
public byte[] BuildDelta(Snapshot cur)
{
// (A) baseline 으로 acked 스냅샷을 찾는다
Snapshot baseSnap = history[ackedTick];
byte[] outBuf = new byte[8];
BitConverter.GetBytes(cur.tick).CopyTo(outBuf, 0);
BitConverter.GetBytes(ackedTick).CopyTo(outBuf, 4);
// (B) cur 와 base 의 차분을 계산해 뒤에 붙인다
AppendDiff(outBuf, baseSnap.state, cur.state);
// 히스토리에 보관
history[cur.tick] = cur;
return outBuf;
}
private void AppendDiff(byte[] outBuf, byte[] baseState, byte[] cur) { /* 차분 인코딩 */ }
}
// ---------------------------------------------------------------------------
// 클라 → 서버: 명령 패킷 리플레이 방어
// ---------------------------------------------------------------------------
public class ReplayGuard
{
// 본 nonce 들을 기억한다
public HashSet<ulong> seenNonces = new HashSet<ulong>();
public const uint kWindowMs = 5000; // 허용 시간 창
// true = 정상 처리, false = 리플레이/만료로 폐기
public bool Accept(byte[] buf, int len, uint serverNowMs)
{
// 헤더: [ulong nonce][uint clientTimeMs]
ulong nonce = BitConverter.ToUInt64(buf, 0);
uint clientTimeMs = BitConverter.ToUInt32(buf, 8);
// (C) 시간 창 검사: 너무 오래된 명령은 버린다
if (serverNowMs - clientTimeMs > kWindowMs)
return false;
// (D) nonce 중복 검사
if (seenNonces.Contains(nonce))
return false;
seenNonces.Add(nonce);
return true; // 통과 → 명령 처리
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 델타 스냅샷 동기화 + 리플레이 방어 (종합/최상)
// ----------------------------------------------------------------------------
// 시나리오(현실적):
// - 서버는 각 클라에 월드 상태를 "델타 스냅샷"으로 보낸다:
// 서버는 tick 마다 스냅샷을 만들고, 클라가 ack 한 baseline 스냅샷과의
// 차분(delta)만 전송한다. 클라는 baseline 에 델타를 적용해 현재 상태를 복원한다.
// 와이어: [uint32 snapTick][uint32 baseTick][delta...]
// - 반대 방향(클라→서버)의 명령 패킷은 리플레이/변조를 막아야 한다:
// 와이어: [uint64 nonce][uint32 clientTimeMs][cmdPayload...][mac(생략)]
// 서버는 nonce 중복과 시간 창(window)으로 리플레이를 거른다.
// - UDP 기반이라 패킷은 순서뒤바뀜/유실/중복/지연되어 도착한다.
// - 클라는 신뢰 불가(치터/리플레이/프록시). 멀티스레드 서버다.
//
// 요구사항:
// - 델타는 클라가 "실제로 가진 baseline"에만 적용되어야 한다. 어긋나면 상태가 발산한다.
// - 같은 명령이 두 번 처리되면(리플레이) 경제/전투 사고가 난다.
// - 어떤 입력/타이밍에도 서버가 죽거나 무한히 메모리를 쓰면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생·악용되는지 입체적으로 설명하고,
// 수정안과 더 나은 설계(델타 동기화 안전성 / 리플레이 방어 / 버전·상태 합의)를 제시하라.
// ============================================================================
#include <cstdint>
#include <cstring>
#include <vector>
#include <unordered_set>
#include <unordered_map>
// ---------------------------------------------------------------------------
// 서버 → 클라: 델타 스냅샷 송신
// ---------------------------------------------------------------------------
struct Snapshot { uint32_t tick; std::vector<uint8_t> state; };
class DeltaSender {
public:
// 클라가 마지막으로 ack 한 baseline tick
uint32_t ackedTick = 0;
// 최근 스냅샷 히스토리 (tick -> snapshot)
std::unordered_map<uint32_t, Snapshot> history;
// 이번 tick 스냅샷을 만들고 델타를 보낸다
std::vector<uint8_t> BuildDelta(const Snapshot& cur)
{
// (A) baseline 으로 acked 스냅샷을 찾는다
const Snapshot& base = history[ackedTick];
std::vector<uint8_t> out(8);
std::memcpy(out.data() + 0, &cur.tick, 4);
std::memcpy(out.data() + 4, &ackedTick, 4);
// (B) cur 와 base 의 차분을 계산해 뒤에 붙인다
AppendDiff(out, base.state, cur.state);
// 히스토리에 보관
history[cur.tick] = cur;
return out;
}
private:
void AppendDiff(std::vector<uint8_t>& out,
const std::vector<uint8_t>& base,
const std::vector<uint8_t>& cur) { /* 차분 인코딩 */ }
};
// ---------------------------------------------------------------------------
// 클라 → 서버: 명령 패킷 리플레이 방어
// ---------------------------------------------------------------------------
#pragma pack(push, 1)
struct CmdHeader {
uint64_t nonce;
uint32_t clientTimeMs;
};
#pragma pack(pop)
class ReplayGuard {
public:
// 본 nonce 들을 기억한다
std::unordered_set<uint64_t> seenNonces;
static constexpr uint32_t kWindowMs = 5000; // 허용 시간 창
// true = 정상 처리, false = 리플레이/만료로 폐기
bool Accept(const uint8_t* buf, size_t len, uint32_t serverNowMs)
{
const CmdHeader* h = reinterpret_cast<const CmdHeader*>(buf);
// (C) 시간 창 검사: 너무 오래된 명령은 버린다
if (serverNowMs - h->clientTimeMs > kWindowMs)
return false;
// (D) nonce 중복 검사
if (seenNonces.count(h->nonce))
return false;
seenNonces.insert(h->nonce);
return true; // 통과 → 명령 처리
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.