Разбор эксплойта производительности серверов UEFN: Как защитить Netcode в Unreal Engine
Каждый разработчик Multiplayer знаком с кошмарным сценарием: один злоумышленник подключается к вашему серверу, выполняет последовательность на первый взгляд безобидных действий, и внезапно ваш tick rate падает с 60 Гц до единичных значений. Весь сервер замирает, что сказывается на десятках ни в чем не повинных игроков.
Недавно на форумах Unreal Engine разработчик Vysena Woyka сообщил о критическом эксплойте производительности серверов UEFN. В отчете описывается 100% воспроизводимая техника, вызывающая серьезную деградацию всего сервера на картах Unreal Editor for Fortnite (UEFN). Тяжесть последствий эксплойта растет по мере присоединения новых игроков, он не требует абсолютно никаких сторонних инструментов и при длительном выполнении может привести к полной нестабильности сервера.
Поскольку точные шаги воспроизведения не разглашаются во избежание массовых злоупотреблений, многие разработчики задаются вопросом: Как такой эксплойт работает «под капотом»? и, что более важно, Как защитить мои собственные кастомные Unreal Engine dedicated servers от подобных атак?
В этом техническом разборе мы препарируем архитектуру деградации производительности на стороне сервера в Unreal Engine. Мы изучим основные векторы, которые злоумышленники используют для «удушения» dedicated servers, узнаем, как реализовать строгую серверную валидацию на C++ и как спроектировать инфраструктуру для максимальной отказоустойчивости.
Анатомия серверного эксплойта в Unreal Engine
Чтобы понять, как игрок может обрушить сервер без внешних хакерских инструментов, нужно разобраться, как Unreal Engine обрабатывает основной игровой цикл (Main Game Loop). Unreal Engine dedicated servers преимущественно однопоточны в том, что касается Game Logic. В то время как задачи вроде Physics Simulation (через физический движок Chaos) и асинхронная загрузка могут быть вынесены в worker threads, основная функция Tick ваших акторов, Replication Serialization и выполнение RPC (Remote Procedure Call) происходят в Game Thread.
Если сервер работает на частоте 30 тиков в секунду (30 Гц), у него есть ровно 33,3 миллисекунды на обработку всех player inputs, обновление Game State, расчет физики и сериализацию сетевых данных для следующего кадра. Если игрок может заставить сервер выполнить операцию, обработка которой занимает 50 миллисекунд, tick rate сервера мгновенно падает до 20 Гц.
Когда 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.
Вектор 1: RPC Flooding и ошибки валидации
Самый распространенный способ уронить или замедлить сервер Unreal Engine — это спам Server RPCs. Если клиент привязывает Server RPC к колесу мыши или к вводу с разблокированной частотой кадров, он может отправлять сотни запросов в секунду на сервер.
Если ваш Server RPC содержит сложную логику — например, спавн актора, выполнение line trace (Raycast) или итерацию по большим массивам — сервер вынужден выполнять эту тяжелую логику сотни раз за кадр.
Unreal Engine предоставляет макрос WithValidation для RPC, но многие разработчики используют его только для проверки валидности указателя, полностью игнорируя Rate Limiting.
Решение: Реализация RPC Rate Limiter на C++
Для защиты сервера необходимо внедрить строгое ограничение частоты (Rate Limiting) для всех коммуникаций между клиентом и сервером. Вот проверенный подход к дросселированию Server RPCs с использованием кастомного Actor Component на C++.
Сначала определим логику ограничения в заголовочном файле:
// 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: Physics и Collision Overload
Еще один распространенный вектор эксплойтов (и один из наиболее вероятных в песочницах вроде UEFN) — перегрузка физики. Если игроки могут спавнить объекты, выбрасывать предметы или манипулировать Physics Bodies, они могут намеренно складывать сотни объектов в ограниченном пространстве.
Когда Physics Bodies перекрываются, физический движок Chaos пытается разрешить коллизии. Если 500 объектов принудительно помещены в одно и то же координатное пространство, математика разрешения коллизий растет экспоненциально, вызывая полную блокировку CPU на сервере.
Более того, если у этих объектов установлено bGenerateOverlapEvents в true, сервер будет вызывать OnComponentBeginOverlap сотни тысяч раз за кадр.
Решение: Агрессивный Collision Culling
Чтобы предотвратить деградацию сервера из-за физики, необходимо отделить визуальную физику от серверной валидации коллизий.
- Отключайте Overlaps для выброшенных предметов: Если игрок выбрасывает предмет, отключите
bGenerateOverlapEventsна сервере после того, как предмет остановится. - Ограничивайте Spawn: Жестко задайте максимальную плотность физических объектов на сектор сетки.
- Дросселируйте логику Overlap: Если вам необходимо использовать оверлапы, не выполняйте сложную логику прямо внутри события перекрытия. Вместо этого установите флаг «dirty» и обработайте оверлап контролируемой пачкой во время функции
Tick.
Вектор 3: Replication Saturation и удушение пропускной способности
Система репликации Unreal Engine мощная, но она также сильно зависит от CPU. Сервер должен итерировать по каждому реплицируемому актору, проверять его релевантность для конкретного клиента, сравнивать его свойства с последним подтвержденным состоянием и сериализовать изменения.
Злоумышленники могут использовать это, быстро меняя реплицируемые переменные (например, данные кастомизации персонажа или состояние инвентаря) туда-сюда. Это заставляет сервер постоянно сериализовать большие блоки данных, насыщая как CPU сервера, так и лимиты пропускной способности.
Решение: Оптимизация NetUpdateFrequency
Никогда не оставляйте 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%, и они упадут.
Если вся архитектура вашего серверного парка уязвима к единой точке отказа, вы рискуете безвозвратно потерять игроков. Построение отказоустойчивой инфраструктуры с правильной маршрутизацией fallback — это то, за что мы активно выступаем, как мы обсуждали в нашем архитектурном разборе The Stop Killing Games Campaign Vs Live Ops Architecting Server Fallbacks.
Когда сервер падает из-за эксплойта, ваш бэкенд должен мгновенно обнаружить «мертвый» узел, запустить новый инстанс и плавно вернуть пострадавших игроков в очередь matchmaking без потери их персистентных данных.
Создание такой системы самостоятельно требует настройки кастомных load balancers, шардирования баз данных, оркестрации контейнеров (например, Kubernetes) и управления SSL-сертификатами — это минимум 4–6 месяцев работы выделенной команды инженеров. С horizOn эти бэкенд-сервисы уже преднастроены. Наша инфраструктура автоматически отслеживает состояние серверов, масштабирует инстансы в зависимости от нагрузки на CPU и управляет маршрутизацией сессий игроков, позволяя вам сосредоточиться на исправлении игрового кода, а не на борьбе с инфраструктурой.
5 лучших практик для стабильности сервера
Чтобы защитить вашу Multiplayer игру на Unreal Engine от эксплойтов производительности, немедленно внедрите эти пять архитектурных правил:
- Внедрите строгие квоты на RPC: Никогда не доверяйте частоте ввода клиента. Используйте компонент C++ rate limiter, описанный выше, чтобы обеспечить жесткие кулдауны для каждого Server RPC.
- Санитизируйте векторы движения: Speed hacks и эксплойты с телепортацией работают путем отправки огромных векторов на сервер. Всегда ограничивайте запросы
AddMovementInputиSetActorLocationна стороне сервера в соответствии с максимальной теоретической скоростью движения персонажа. - Используйте Replication Graph: Если ваша игра поддерживает более 40 игроков, стандартная система репликации станет узким местом. Внедрите Unreal Engine Replication Graph для пространственной группировки акторов и радикального снижения нагрузки на CPU при проверках релевантности.
- Отключите визуализацию на стороне сервера: Dedicated servers никогда не должны загружать UI, системы частиц или анимации skeletal mesh. Убедитесь, что настройки вашего проекта строго исключают эти ассеты из билда dedicated server для освобождения памяти и циклов CPU.
- Динамически отслеживайте Tick Rate: Реализуйте серверную подсистему, которая мониторит среднее дельта-время. Если сервер обнаруживает падение tick rate ниже 15 Гц более чем на 5 секунд, он должен автоматически приостановить второстепенные фоновые задачи (такие как спавн AI или генерация амбиентных событий) для восстановления.
Заключение
Недавний эксплойт производительности серверов UEFN — это суровое напоминание о том, что разработка Multiplayer игр — это, по сути, упражнение в кибербезопасности. Вы не можете просто доверять тому, что игроки будут взаимодействовать с вашей игрой так, как задумано. Каждый RPC, каждое физическое взаимодействие и каждая реплицируемая переменная — это потенциальный вектор атаки.
Перейдя к модели «Server-Authoritative, Client-Distrusted», глубоко оптимизировав логику репликации на C++ и внедрив строгие ограничения частоты, вы сможете защитить свою игру от подобных катастрофических падений производительности.
Когда вы сочетаете пуленепробиваемый игровой код с самомасштабируемой и самовосстанавливающейся серверной инфраструктурой, вы создаете среду, в которой эксплойты становятся мелкими неприятностями, а не катастрофами, убивающими игру. Готовы масштабировать свой Multiplayer бэкенд без головной боли с DevOps? Попробуйте horizOn бесплатно, и мы возьмем на себя оркестрацию ваших серверов.
Источник: [CRITICAL] Server Performance Exploit