Dedicated Servers가 프리징되는 이유: Unreal Engine 서버 DDoS Protection의 현실
모든 Multiplayer 게임 개발자는 갑작스럽고 설명할 수 없는 서버 프리징을 두려워합니다. 당신의 전용 인스턴스가 안정적인 30 Tick rate로 완벽하게 실행되고 있다가, 예고도 없이 전체 시뮬레이션이 중단됩니다. 플레이어들은 맵 곳곳에서 Rubber-banding 현상을 겪고, RPCs는 유실되며, 잠시 후 치명적인 Connection timeout으로 매치가 종료됩니다. 본능적으로 최신 Movement replication 코드나 복잡한 물리 계산을 탓할 수도 있지만, 플레이어 기반이 성장하고 있다면 현실은 훨씬 더 악의적일 때가 많습니다. 바로 당신의 인프라가 조직적인 Distributed Denial of Service (DDoS) 공격의 희생양이 된 것입니다.
최근 Unreal Engine 개발자 커뮤니티의 보고에 따르면 게임 서버를 겨냥한 조직적인 DDoS 공격이 급증하고 있으며, 특히 배틀로얄(Battle Royale)이나 커스텀 Creative 인스턴스와 같은 대규모 모드에 큰 영향을 미치고 있습니다. 이러한 공격은 서버의 Network processing thread를 완전히 마비시켜 심각한 Lag, 글로벌 동기화 오류, 그리고 궁극적으로는 Hard crashes를 유발합니다.
Indie 개발자와 AA 스튜디오에게 강력한 Unreal Engine Server DDoS Protection을 구현하는 것은 이제 선택이 아닌 필수 사항입니다. 이 기술 분석에서는 이러한 공격이 Unreal Engine Netcode를 어떻게 조작하는지, 악성 플러드(Flood)와 일반적인 네트워크 불량 상태를 어떻게 구별하는지, 그리고 게임 인프라를 강화하기 위해 취할 수 있는 구체적인 단계들을 살펴봅니다.
Unreal Engine 서버 크래시의 해부
서버를 보호하는 방법을 이해하려면 먼저 Unreal Engine이 수신 네트워크 트래픽을 처리하는 방식을 알아야 합니다. Unreal은 NetDriver에 의해 관리되는 커스텀 UDP 기반 프로토콜을 사용합니다. UDP는 비연결형 프로토콜이므로 인터넷상의 모든 클라이언트가 공식적인 Handshake 없이 서버의 오픈 포트로 패킷을 보낼 수 있습니다.
레이어 4 볼륨 공격 vs 레이어 7 애플리케이션 공격
대부분의 서버 크래시는 다음 두 가지 유형의 네트워크 공격 중 하나에 의해 발생합니다.
1. 볼륨 UDP 플러드 (레이어 4): 이것은 무차별 대입 공격입니다. Botnet이 서버의 공용 IP 주소와 포트에 초당 수 기가바이트의 가비지 UDP 패킷을 퍼붓습니다. 서버의 네트워크 인터페이스 카드(NIC)와 운영 체제의 Network stack이 완전히 포화 상태가 됩니다. Unreal Engine이 패킷을 살펴볼 기회조차 갖기 전에 하부 하드웨어의 Bandwidth가 소진되거나 CPU 인터럽트(CPU interrupts)가 발생하여 정당한 플레이어 트래픽이 완전히 차단됩니다.
2. 애플리케이션 계층 고갈 (레이어 7):
이러한 공격은 훨씬 더 교활합니다. 공격자는 무작위 가비지를 보내는 대신 Packet capture 도구나 변조된 게임 클라이언트를 사용하여 올바른 형식의 Unreal Engine 연결 요청(NMT_Hello 또는 NMT_Login 패킷 등)이나 특정 무거운 RPC 스팸을 보냅니다. NetDriver는 이러한 겉보기에 유효한 패킷을 수락하고 처리를 위해 Game thread로 전달합니다. 서버 CPU는 수천 개의 가짜 로그인 Handshake를 파싱하고, 존재하지 않는 세션 티켓을 검증하거나, 복제된 함수의 복잡한 문자열 매개변수를 위한 메모리를 할당하려고 시도하면서 100%까지 치솟습니다. 이 트래픽은 표준 Firewall에 정당한 플레이어 활동과 동일하게 보이므로 기본 DDoS 방어를 우회합니다. 이는 즉시 서버 Tick rate를 떨어뜨려, Watchdog 프로세스가 프리징된 인스턴스를 강제 종료하기 직전에 플레이어들이 겪는 극심한 텔레포트 및 Rollback 현상을 유발합니다.
공격 진단: 악의적인 공격인가, 아니면 단순히 나쁜 Netcode인가?
서버가 공격받고 있다고 가정하기 전에 치명적인 Replication 버그를 배제해야 합니다. 단일 클라이언트가 RPC 호출의 무한 루프를 트리거하면 레이어 7 DDoS와 유사한 증상을 보일 수 있습니다. 패닉에 빠지기 전에 크래시 로그와 메트릭을 검토하십시오. Memory allocation이 크게 급증하지만 네트워크 트래픽은 낮다면 Replication 문제일 수 있습니다. 이에 대한 지침은 Zero Ping Spikes Complete Freeze The Ultimate Uefn Server Crash Fix Protocol 가이드를 확인하십시오.
하지만 외부 모니터링에서 인바운드 트래픽이 평소 ~50 Mbps에서 5 Gbps로 급증하거나, 서버 로그에서 단 몇 초 만에 고유 IP 주소로부터 수천 개의 LogNet: NotifyAcceptingConnection 메시지가 표시된다면 조직적인 공격을 받고 있는 것입니다.
Netcode 강화: C++로 연결 스로틀링 구현하기
진정한 볼륨 DDoS 완화는 인프라 수준에서 이루어져야 하지만(잠시 후 설명), AGameModeBase에서 직접 공격적인 Rate Limiting을 구현하여 레이어 7 애플리케이션 고갈로부터 Unreal Engine 서버를 보호할 수 있습니다.
PreLogin 함수를 오버라이드함으로써 서버가 전체 APlayerController를 할당하고 플레이어를 월드에 로드하는 비용이 많이 드는 프로세스를 시작하기 전에 연결 시도를 가로챌 수 있습니다.
악성 IP 주소로부터의 급격한 연결 시도를 스로틀링하는 강력한 C++ 구현 예시는 다음과 같습니다.
// In YourGameMode.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "YourGameMode.generated.h"
UCLASS()
class YOURGAME_API AYourGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
private:
// Maps to track connection attempts per IP
TMap<FString, int32> ConnectionAttempts;
TMap<FString, float> LastConnectionTime;
// Configuration limits
const int32 MaxAttemptsPerMinute = 4;
const float LockoutTimeSeconds = 60.0f;
};
// In YourGameMode.cpp
#include "YourGameMode.h"
#include "Engine/World.h"
void AYourGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
// Always call super first to handle native bans and base logic
Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
// If an error was already generated (e.g., server full), exit early
if (!ErrorMessage.IsEmpty())
{ return;
}
// The Address string usually arrives in the format "IP:Port"
FString ClientIP;
FString PortStr;
if (!Address.Split(TEXT(":"), &ClientIP, &PortStr))
{ ClientIP = Address; // Fallback if no port is appended
}
float CurrentTime = GetWorld()->GetTimeSeconds();
// Check if this IP is currently in our tracking map
if (LastConnectionTime.Contains(ClientIP))
{ float TimeSinceLastAttempt = CurrentTime - LastConnectionTime[ClientIP];
// If they are connecting too fast and have exceeded the attempt limit
if (TimeSinceLastAttempt < LockoutTimeSeconds && ConnectionAttempts[ClientIP] >= MaxAttemptsPerMinute)
{ ErrorMessage = TEXT("Connection rate limit exceeded. Please wait 60 seconds.");
UE_LOG(LogGameMode, Warning, TEXT("DDoS Mitigation: Rejected rapid connection attempt from %s."), *ClientIP);
return;
}
// If the lockout window has passed, reset their counter
if (TimeSinceLastAttempt >= LockoutTimeSeconds)
{ ConnectionAttempts[ClientIP] = 0;
} }
// Increment the attempt counter and update the timestamp
int32 Attempts = ConnectionAttempts.FindOrAdd(ClientIP, 0);
ConnectionAttempts[ClientIP] = Attempts + 1;
LastConnectionTime.Add(ClientIP, CurrentTime);
UE_LOG(LogGameMode, Log, TEXT("Connection validation passed. Attempt %d from %s"), ConnectionAttempts[ClientIP], *ClientIP);
}
이 코드가 중요한 이유
이 구현은 들어오는 모든 요청의 IP 주소를 추적합니다. 단일 IP가 60초 이내에 4회 이상 연결을 시도하면 서버는 PreLogin에서 능동적으로 연결을 거부합니다. 여기서 연결을 거부하는 것은 엔진이 액터를 생성하고, 초기 상태를 복제하고, 플레이어를 킥하도록 허용하는 것보다 CPU 사이클 측면에서 훨씬 저렴합니다. 이 간단한 코드 블록이 서버가 레이어 7 스크립트 키디 공격에서 살아남느냐, 아니면 완전히 응답 없는 상태로 크래시가 발생하느냐의 차이를 만들 수 있습니다.
Unreal Engine 네트워크 구성 튜닝
C++ 로직 외에도 DefaultEngine.ini 파일에는 서버가 소비할 수 있는 대역폭을 결정하는 몇 가지 중요한 구성 매개변수가 포함되어 있습니다. 이를 기본 설정으로 두는 것은 심각한 취약점입니다. 공격자가 서버에 플러딩을 하고 Bandwidth 제한이 없다면, 서버는 모든 것을 처리하려고 시도하여 순식간에 CPU 점유율을 최대로 끌어올릴 것입니다.
네트워크 트래픽에 대해 엄격한 상한선을 설정해야 합니다. DefaultEngine.ini를 열고 IpNetDriver에 다음 강화된 제한 사항을 적용하십시오.
[/Script/Engine.Player]
; Limit maximum connection speed to 10 MB/s to prevent single-client bandwidth exhaustion
ConfiguredInternetSpeed=10485760
ConfiguredLanSpeed=10485760
[/Script/OnlineSubsystemUtils.IpNetDriver]
; Maximum data rate allowed per client (in bytes). 100kb/s is usually plenty for an FPS.
MaxClientRate=100000
MaxInternetClientRate=100000
; Cap the server tick rate to ensure predictable CPU load.
NetServerMaxTickRate=30
; Aggressively drop unresponsive clients. Defaults are often too long (60s+).
ConnectionTimeout=15.0
InitialConnectTimeout=15.0
; How often the server expects a keep-alive ping.
KeepAliveTime=0.2
; Limit the number of ports the server will try to bind to upon startup.
MaxPortCountToTry=512
ConnectionTimeout을 15.0초로 줄임으로써 서버는 스푸핑된 DDoS 공격으로 생성된 절반만 열린 연결이나 데드 연결을 신속하게 제거하여 정당한 플레이어를 위한 메모리와 네트워크 슬롯을 확보할 수 있습니다.
인프라 문제: 이미 도착한 패킷은 차단할 수 없다
위에서 설명한 C++ 스스로틀링 및 INI 구성은 애플리케이션 계층 고갈로부터 보호해주지만, 레이어 4 볼륨 공격과 관련해서는 치명적인 결함이 있습니다. 바로 Unreal Engine 서버가 패킷을 드롭하기로 결정할 때쯤에는 이미 대역폭이 소모된 상태라는 것입니다.
공격자가 서버에 10 Gbps Botnet 공격을 가하고 호스팅 제공업체가 1 Gbps 네트워크 인터페이스만 제공한다면, C++ 코드가 아무리 최적화되어 있어도 소용없습니다. 서버로 이어지는 파이프가 물리적으로 막혀버리기 때문입니다. 정당한 플레이어 트래픽이 통과할 수 없게 되어 최근 커뮤니티 보고에서 설명된 대규모 동기화 오류 및 텔레포트 현상이 발생합니다.
레이어 4 공격을 완화하려면 인프라 수준의 방어 전략이 필요합니다.
DIY (직접 구축) 방식
자체 전용 베어메탈 서버나 표준 EC2 인스턴스를 운영하는 경우 완화 파이프라인을 수동으로 구축해야 합니다. 여기에는 일반적으로 다음이 포함됩니다.
- 리버스 프록시(Reverse Proxy) 설정: 실제 Unreal Engine 서버 IP를 외부에 노출해서는 안 됩니다. UDP 프록시(UDP 포워딩을 위해
stream모듈이 구성된 NGINX 또는 HAProxy 등)를 통해 트래픽을 라우팅해야 합니다. 이는 홉(hop) 지연을 추가하지만 UE 바이너리를 실행하는 컴퓨팅 인스턴스의 실제 IP를 숨길 수 있게 해줍니다. - iptables/nftables 구성: 커널 수준에서 파편화된 UDP 패킷을 드롭하고 IP당 연결을 제한하는 엄격한 방화벽 규칙을 작성해야 합니다.
- 엔터프라이즈 완화 서비스 구매: 악성 트래픽이 데이터 센터에 도달하기 전에 정화하기 위해 고가의 엔터프라이즈 라우팅 서비스(AWS Shield Advanced 또는 Cloudflare Magic Transit 등)를 구입해야 합니다.
이러한 복원력 있는 프록시 기반 아키텍처를 직접 구축하려면 플릿 매니저, 로드 밸런서(Load balancers) 및 복잡한 라우팅 테이블을 설정해야 하며, 이는 전문 DevOps 인력의 4-6개월 작업 분량입니다. 게임 출시를 준비하는 Indie 스튜디오에게는 엄청난 재정적, 시간적 낭비입니다.
DevOps 함정에서 벗어나기
이것이 바로 Backend-as-a-Service 플랫폼이 해결하도록 설계된 인프라의 악몽입니다. horizOn을 사용하면 이 강화된 백엔드 인프라가 사전 구성된 상태로 제공됩니다.
몇 달 동안 iptables를 구성하고 볼륨 UDP 플러드를 걱정하는 대신, 저희 플랫폼이 Edge network를 대신 관리해 드립니다. 당신의 게임 인스턴스는 엔터프라이즈급 라우팅 계층 뒤에 보호되며, 이 계층은 악성 레이어 4 및 레이어 7 트래픽이 실제 Unreal Engine 서버 스레드에 도달하기 전에 자동으로 식별하고 드롭합니다. 즉, Tick rate가 안정적으로 유지되고, 정당한 플레이어는 연결을 유지할 수 있으며, 당신은 봇넷 완화가 아닌 게임플레이 코드 작성에 집중할 수 있습니다.
공격받고 있는 Indie 개발자를 위한 4가지 모범 사례
자체 완화 스택을 구축하든 관리형 인프라에 의존하든, 게임을 강화하려면 다음 핵심 보안 원칙을 따라야 합니다.
1. 서버 IP를 클라이언트에 직접 노출하지 마십시오: 플레이어가 Wireshark를 열어 서버 IP를 볼 수 있다면 공격자도 볼 수 있습니다. 연결을 중개하거나 세션 티켓을 사용하는 보안 Matchmaking 서비스를 활용해야 합니다. 클라이언트는 보호된 릴레이나 프록시를 통해 연결해야 합니다.
2. 엄격한 세션 검증 구현:
IP와 포트만 안다고 해서 클라이언트를 연결시키지 마십시오. 연결 문자열의 옵션으로 백엔드에서 생성된 단기 암호화 토큰(JWT 등)을 전달하도록 요구하십시오. PreLogin에서 비동기 HTTP 호출을 통해 이 토큰을 즉시 검증하십시오. 이는 공격자가 IP 주소를 교체하는 것만으로 속도 제한을 우회하는 것을 방지합니다.
3. 서버 Tick Rate 제한:
제한 없는 NetServerMaxTickRate를 실행하지 마십시오. 예상치 못한 트래픽 급증에 대비해 CPU 여유 공간을 확보할 수 있도록 예측 가능한 값(30Hz)으로 고정하십시오.
4. 엔진뿐만 아니라 네트워크 에지(Network Edge)를 모니터링하십시오:
엔진 크래시 로그는 방화벽 수준에서 드롭된 패킷에 대해 알려주지 않습니다. OS 수준에서 인바운드 대역폭(InBytes)과 패킷 속도를 추적하는 메트릭이 있어야 합니다. 플레이어 수 증가와 상관없는 인바운 UDP 트래픽의 갑작스러운 급증은 볼륨 공격의 주요 지표입니다.
Multiplayer 게임을 악의적인 행위자로부터 보호하는 것은 끊임없는 군비 경쟁입니다. 코드에 공격적인 속도 제한을 구현하고, 엔진 구성을 강화하며, 에지에서 나쁜 트래픽을 드롭하는 인프라를 활용함으로써 플레이어에게 설계한 그대로의 게임 경험을 제공할 수 있습니다.
서버 인프라 걱정은 그만두고 게임 개발에 집중할 준비가 되셨나요? horizOn을 무료로 체험하고 저희의 복원력 있는 백엔드 아키텍처가 압박 속에서도 어떻게 게임을 온라인으로 유지하는지 확인해 보십시오.
출처: [VERY CRITICAL] Organized DDoS Attacks Causing Server Crashes