UEFNサーバーのパフォーマンス脆弱性の解説:Unreal EngineのNetcodeを堅牢にする方法
すべてのMultiplayer開発者は、悪夢のようなシナリオを知っています。一人の悪意あるプレイヤーがサーバーに接続し、一見無害に見える一連のアクションを実行しただけで、サーバーのtick rateが60Hzから一桁台に急落するのです。サーバー全体が停止し、数十人の罪のないプレイヤーが影響を受けます。
最近、開発者のVysena Woyka氏によって、Unreal Engineのフォーラムで重大なUEFNサーバーパフォーマンスの脆弱性(exploit)が報告されました。この報告書では、Unreal Editor for Fortnite (UEFN) のマップにおいて、サーバー全体の深刻な劣化を引き起こす、100%再現可能な手法が概説されています。この脆弱性はプレイヤーが参加するほど深刻度が増し、サードパーティ製のツールを一切必要とせず、長時間実行するとサーバーが完全に不安定になる可能性があります。
広範囲にわたる悪用を防ぐために正確な再現手順は非公開とされていますが、多くの開発者は「このような脆弱性は内部でどのように機能しているのか?」、さらに重要なことに「自分のカスタムUnreal Engine dedicated serversを同様の攻撃からどう守ればよいのか?」という疑問を抱いています。
このテクニカルディープダイブでは、Unreal Engineにおけるサーバーサイドのパフォーマンス劣化のアーキテクチャを解剖します。悪意のあるプレイヤーがdedicated serversを窒息させるために使用する一般的なベクトル、C++を使用した厳格なサーバーサイドバリデーションの実装方法、そして最大のレジリエンス(回復力)を実現するためのインフラ構築方法について探ります。
Unreal Engineサーバー脆弱性の解剖学
外部のハッキングツールを使わずにプレイヤーがサーバーをダウンさせることができる理由を理解するには、Unreal Engineがメインのゲームループをどのように処理しているかを理解する必要があります。Unreal Engine dedicated serversは、Game Logicに関しては主にシングルスレッドで動作します。物理シミュレーション(Chaos物理エンジン経由)や非同期ロードなどのタスクはワーカータレッドにオフロードできますが、Actorのコアとなる Tick 関数、Replication Serialization、およびRPC (Remote Procedure Call) の実行はすべてGame Thread上で行われます。
サーバーが毎秒30ティック(30Hz)で動作している場合、すべてのplayer inputsを処理し、Game Stateを更新し、物理を計算し、次のフレームのためにネットワークデータをシリアライズするのに、正確に33.3ミリ秒の猶予があります。もしプレイヤーが処理に50ミリ秒かかる操作をサーバーに強制的に実行させることができれば、サーバーのtick rateは即座に20Hzに低下します。
サーバーのtick rateがこれほど劇的に低下すると、単なる視覚的なラグだけでなく、壊滅的な状態の乖離(state divergence)が発生します。これによる影響については、テクニカルガイド「The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It」で詳しく解説しています。
メモリインジェクターやパケットエディターを使用しないゲーム内のパフォーマンス脆弱性は、通常、RPC Flooding、Physics/Collision Overload、またはReplication Saturationの3つのベクトルのいずれかに依存しています。
ベクトル1:RPC Floodingとバリデーションの失敗
Unreal Engineサーバーをクラッシュさせたり劣化させたりする最も一般的な方法は、Server RPCsをスパムすることです。クライアントがServer RPCをマウスホイールやフレームレート制限のない入力にバインドすると、サーバーに対して毎秒数百のリクエストを送信できてしまいます。
Server RPCに、Actorのスポーン、ライントレース(Raycast)の実行、大きな配列の反復処理などの複雑なロジックが含まれている場合、サーバーはフレームごとにその高負荷なロジックを数百回実行することを強いられます。
Unreal EngineはRPC用に WithValidation マクロを提供していますが、多くの開発者はポインタが有効かどうかを確認するためだけにこれを使用し、Rate Limitingを完全に無視しています。
解決策:C++ RPC Rate Limiterの実装
サーバーを保護するには、すべてのクライアントからサーバーへの通信に対して厳格なRate Limitingを実装する必要があります。以下は、C++のカスタムActor Componentを使用してServer RPCsをスロットリングする、実戦でテスト済みの手法です。
まず、ヘッダーファイルでレート制限ロジックを定義します。
// RateLimiterComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "RateLimiterComponent.generated."
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MULTIPLAYER_API URateLimiterComponent : public UActorComponent
{
GENERATED_BODY()
public:
URateLimiterComponent();
// Checks if the action is allowed. Returns false if the client is spamming.
UFUNCTION(BlueprintCallable, Category = "Security")
bool CanExecuteAction(FName ActionName, float CooldownTime);
private:
// Maps action names to the last time they were executed
TMap<FName, float> LastExecutionTimes;
// Threshold for maximum allowed actions per second before flagging the player
const int32 MaxActionsPerSecond = 20;
int32 CurrentActionCount;
float LastResetTime;
};
次に、CPPファイルでバリデーションロジックを実装します。クライアントがローカル時間を偽装してクールダウンを回避できないように、サーバーの時間(GetWorld()->GetTimeSeconds())を使用していることに注目してください。
// RateLimiterComponent.cpp
#include "RateLimiterComponent.h"
URateLimiterComponent::URateLimiterComponent()
{
PrimaryComponentTick.bCanEverTick = false;
CurrentActionCount = 0;
LastResetTime = 0.0f;
}
bool URateLimiterComponent::CanExecuteAction(FName ActionName, float CooldownTime)
{
// Only run this logic on the server
if (!GetOwner()->HasAuthority())
{
return false;
}
float CurrentTime = GetWorld()->GetTimeSeconds();
// Reset the global action counter every second
if (CurrentTime - LastResetTime >= 1.0f)
{
CurrentActionCount = 0;
LastResetTime = CurrentTime;
}
// Global spam check
CurrentActionCount++;
if (CurrentActionCount > MaxActionsPerSecond)
{
UE_LOG(LogTemp, Warning, TEXT("Player %s is exceeding global RPC limits!"), *GetOwner()->GetName());
return false;
}
// Specific action cooldown check
if (LastExecutionTimes.Contains(ActionName))
{
float LastTime = LastExecutionTimes[ActionName];
if (CurrentTime - LastTime < CooldownTime)
{
// Client is spamming this specific action
return false;
}
}
// Update the execution time and allow the action
LastExecutionTimes.Add(ActionName, CurrentTime);
return true;
}
これで、Server_PerformAction_Validate 関数を実装する際に、クライアントがRPCをスパムしている場合に動的に拒否できるようになります。
bool AMyPlayerController::Server_PerformExpensiveAction_Validate()
{
// If the rate limiter returns false, the RPC is rejected and the client is disconnected
if (URateLimiterComponent* RateLimiter = GetComponentByClass<URateLimiterComponent>())
{
return RateLimiter->CanExecuteAction(FName("ExpensiveAction"), 0.5f);
}
return true;
}
ベクトル2:物理とコリジョンの過負荷
もう一つの一般的な脆弱性ベクトル(そしてUEFNのようなサンドボックス環境で強く疑われているもの)は、物理の過負荷です。プレイヤーがオブジェクトをスポーンしたり、アイテムをドロップしたり、物理ボディを操作したりできる場合、狭いスペースに意図的に数百のオブジェクトを積み上げることができます。
物理ボディが重なると、Chaos物理エンジンはコリジョンの解消を試みます。500個のオブジェクトが同じ座標空間に強制的に押し込まれると、コリジョン解消の計算が指数関数的に増大し、サーバーのCPUが完全にロックアップします。
さらに、これらのオブジェクトの bGenerateOverlapEvents がtrueに設定されている場合、サーバーはフレームごとに OnComponentBeginOverlap を数十万回実行することになります。
解決策:アグレッシブなコリジョンカリング
物理ベースのサーバー劣化を防ぐには、ビジュアル的な物理とサーバーサイドのコリジョンバリデーションを切り離す必要があります。
- ドロップアイテムのオーバーラップを無効化: プレイヤーがアイテムをドロップした場合、静止した後にサーバー側で
bGenerateOverlapEventsを無効にします。 - スポーン制限の設定: グリッドセクターあたりの物理オブジェクトの最大密度をハードコードします。
- オーバーラップロジックの節流: オーバーラップを使用する必要がある場合は、オーバーラップイベント内で直接複雑なロジックを実行しないでください。代わりにダーティフラグを立て、
Tick関数中に制御されたバッチでオーバーラップを処理します。
ベクトル3:Replication Saturationと帯域幅の圧迫
Unreal Engineのレプリケーションシステムは強力ですが、CPUへの依存度も高いです。サーバーは、レプリケートされたすべてのActorを反復処理し、特定のクライアントに関連があるかどうかを確認し、プロパティを最後に承認された状態と比較し、変更をシリアライズする必要があります。
悪意のあるプレイヤーは、レプリケートされた変数(キャラクターのカスタマイズデータやインベントリの状態など)を高速に書き換えることで、これを悪用できます。これにより、サーバーは常に大量のデータをシリアライズすることを強いられ、サーバーのCPUと帯域幅制限の両方が飽和状態になります。
解決策:NetUpdateFrequencyの最適化
重要でないActorの NetUpdateFrequency をデフォルト値(100.0)のままにしないでください。プレイヤーの近接度やアクションの状態に基づいて、レプリケーションの頻度を動的にスケーリングする必要があります。
さらに、DefaultEngine.ini を使用して、dedicated server に厳格な帯域幅制限を課す必要があります。これにより、単一の悪意のあるクライアントがサーバーに大量のパケットストリームを処理させるのを防ぐことができます。
[/Script/OnlineSubsystemUtils.IpNetDriver]
MaxClientRate=15000
MaxInternetClientRate=10000
NetServerMaxTickRate=30
LanServerMaxTickRate=30
ConnectionTimeout=15.0
InitialConnectTimeout=30.0
MaxClientRate を制限することで、ネットワークチャネルを溢れさせようとするクライアントからの過剰なパケットをサーバーが単に破棄し、正当なプレイヤーのためにCPUサイクルを維持できます。
インフラのレジリエンス:不可避な事態への対処
完璧なC++コードを書いても、ゼロデイ脆弱性は発生します。UEFNサーバーパフォーマンスバグのような脆弱性がカスタムゲームを襲ったとき、サーバーノードは必然的にCPU使用率が100%に達し、クラッシュします。
サーバーフリート全体のアーキテクチャが単一障害点に対して脆弱である場合、プレイヤーの永久的な離脱を招くリスクがあります。適切なフォールバックルーティングを備えたレジリエントなインフラを構築することは、私たちが強く推奨していることです。これについては、The Stop Killing Games Campaign Vs Live Ops Architecting Server Fallbacks のアーキテクチャ分析でも議論しました。
脆弱性によってサーバーがクラッシュしたとき、バックエンドは即座に停止したノードを検出し、新しいインスタンスを立ち上げ、影響を受けたプレイヤーを永続データを失うことなく matchmaking キューにスムーズに移行させる必要があります。
これを自前で構築するには、カスタムロードバランサー、データベースシャarding、コンテナオーケストレーション(Kubernetesなど)、SSL証明書管理の設定が必要であり、優に4〜6ヶ月の専任エンジニアリング作業が必要です。horizOn を使用すれば、これらのバックエンドサービスは事前に構成されています。当社のインフラはサーバーの健全性を自動的に監視し、CPU負荷に基づいてインスタンスを自動スケーリングし、プレイヤーセッションのルーティングを処理するため、インフラとの戦いではなく、ゲームコードの修正に集中できます。
サーバー安定性のための5つのベストプラクティス
Unreal Engineのマルチプレイヤーゲームをパフォーマンス脆弱性から守るために、以下の5つのアーキテクチャルールを直ちに実装してください。
- 厳格なRPCクォータの実装: クライアントの入力レートを決して信用しないでください。前述のC++レートリミッターコンポーネントを使用して、すべてのServer RPCにハードなクールダウンを強制します。
- 移動ベクトルのサニタイズ: スピードハックやテレポートの脆弱性は、巨大なベクトルをサーバーに送信することで機能します。サーバー側で、キャラクターの理論上の最大移動速度に対して
AddMovementInputおよびSetActorLocationリクエストを常にクランプしてください。 - Replication Graphの活用: ゲームが40人以上のプレイヤーをサポートする場合、デフォルトのレプリケーションシステムがボトルネックになります。Unreal Engine Replication Graphを実装してActorを空間的にグループ化し、関連性チェックのCPUオーバーヘッドを劇的に削減します。
- サーバーサイドのビジュアルを無効化: Dedicated servers は、UI、パーティクルシステム、またはスケルタルメッシュアニメーションをロードすべきではありません。メモリとCPUサイクルを解放するために、プロジェクト設定でこれらのアセットを dedicated server ビルドから厳密に除外してください。
- Tick Rateの動的監視: 平均デルタタイムを監視するサーバーサイドのサブシステムを実装します。サーバーがtick rateが5秒以上にわたって15Hzを下回っていることを検出した場合、回復のために重要でないバックグラウンドタスク(AIのスポーンや環境イベントの生成など)を自動的に一時停止する必要があります。
結論
最近のUEFNサーバーパフォーマンスの脆弱性は、マルチプレイヤーゲーム開発が本質的にサイバーセキュリティの演習であることを痛感させます。プレイヤーが意図した通りにゲームを操作すると単純に信じることはできません。すべてのRPC、すべての物理インタラクション、そしてすべてのレプリケートされた変数が潜在的な攻撃ベクトルです。
「サーバー権威、クライアント不信(Server-Authoritative, Client-Distrusted)」モデルに考え方を切り替え、C++のレプリケーションロジックを深く最適化し、厳格なレート制限を実装することで、このような壊滅的なパフォーマンスクラッシュからゲームを武装させることができます。
鉄壁のゲームコードと、自動スケーリングおよび自己修復機能を備えたサーバーインフラを組み合わせることで、脆弱性がゲームを台無しにする惨事ではなく、単なる小さな迷惑行為で済む環境を構築できます。DevOpsの煩わしさなしにマルチプレイヤーバックエンドをスケールさせる準備はできましたか? horizOn を無料でお試しください。サーバーのオーケストレーションは私たちにお任せください。