为什么你的 Dedicated Servers 会卡死:Unreal Engine 服务器 DDoS Protection 的现状
每一个 Multiplayer 游戏开发者都害怕突然出现的、莫名其妙的服务器冻结。你的专用实例正以稳定的 30 Tick rate 完美运行,突然之间,在没有任何预警的情况下,整个模拟戛然而止。玩家在地图上疯狂 Rubber-banding,RPCs 丢失,几秒钟后,致命的 Connection timeout 终止了比赛。你可能会本能地责怪你最新的 Movement replication 代码或复杂的物理计算,但如果你的玩家基数正在增长,现实往往要险恶得多:你的基础设施正遭受协同式分布式拒绝服务 (DDoS) 攻击。
来自 Unreal Engine 开发者社区的最新报告指出,针对游戏服务器的有组织 DDoS 攻击大幅增加,特别是影响到大逃杀 (Battle Royale) 等大规模模式和自定义 Creative 实例。这些攻击会完全压垮服务器的 Network processing thread,导致严重的 Lag、全局去同步,并最终导致 Hard crashes。
对于 Indie 开发者和 AA 工作室来说,实施强大的 Unreal Engine Server DDoS Protection 已不再是可选项,而是任何 Live-ops 游戏的强制性要求。在本次技术解析中,我们将分析这些攻击如何操纵 Unreal Engine Netcode,如何区分恶意攻击流量与普通的网络状况不佳,以及你可以采取哪些具体步骤来加固你的游戏基础设施。
Unreal Engine 服务器崩溃解析
要了解如何保护你的服务器,首先需要了解 Unreal Engine 如何处理传入的网络流量。Unreal 使用由 NetDriver 管理的自定义 UDP 协议。由于 UDP 是无连接的,互联网上的任何客户端都可以在没有正式 Handshake 的情况下向服务器的开放端口发送数据包。
Layer 4 流量攻击 vs. Layer 7 应用层攻击
大多数服务器崩溃是由以下两种网络攻击之一造成的:
1. 容积式 UDP 洪泛攻击 (Layer 4): 这是一种暴力攻击。Botnet 以每秒数 GB 的垃圾 UDP 数据包轰炸服务器的公网 IP 地址和端口。服务器的网卡 (NIC) 和操作系统的 Network stack 会完全饱和。在 Unreal Engine 甚至还没机会查看这些数据包之前,底层机器的 Bandwidth 就会耗尽或 CPU 中断异常,导致合法的玩家流量被完全丢弃。
2. 应用层耗尽攻击 (Layer 7):
这类攻击更具隐蔽性。攻击者不是发送随机垃圾,而是使用 Packet capture 工具或修改过的游戏客户端发送格式正确的 Unreal Engine 连接请求(如 NMT_Hello 或 NMT_Login 数据包)或特定的重型 RPC 垃圾邮件。NetDriver 接受这些看似有效的数据包并将其交给 Game thread 处理。服务器 CPU 会飙升至 100%,因为它试图解析成千上万个虚假的登录 Handshakes,验证不存在的会话令牌,或为复制函数中的复杂字符串参数分配内存。由于这些流量对于标准 Firewall 来说与合法玩家活动完全相同,因此它会绕过基础的 DDoS 防护。这会立即导致服务器 Tick rate 暴跌,造成玩家在服务器 Watchdog 进程杀死冻结实例之前经历的极端传送和 Rollback 行为。
诊断攻击:是恶意攻击还是 Netcode 太烂?
在假设你的服务器受到攻击之前,你必须排除灾难性的 Replication 漏洞。如果单个客户端触发了 RPC 调用的无限循环,它可能会模仿 Layer 7 DDoS。在进入恐慌模式之前,你应该检查你的崩溃日志和指标。如果你看到内存分配大幅飙升但网络流量较低,你可能遇到的是 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 来保护你的 Unreal Engine 服务器免受 Layer 7 应用层耗尽攻击。
通过重写 PreLogin 函数,你可以在服务器分配完整的 APlayerController 并开始加载玩家到世界的昂贵过程之前拦截连接尝试。
以下是一个强大的 C++ 实现,用于节流来自恶意 IP 地址的快速连接尝试:
// 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 中主动拒绝连接。在这里拒绝连接比允许引擎生成 Actor、复制初始状态然后踢出玩家要节省大量的 CPU 周期。这段简单的代码块可能决定了你的服务器是在 Layer 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 配置将保护你免受应用层耗尽,但面对 Layer 4 容积式攻击时,它们有一个致命的弱点:当你的 Unreal Engine 服务器决定丢弃数据包时,Bandwidth 已经被消耗了。
如果攻击者将 10 Gbps 的 Botnet 对准你的服务器,而你的托管提供商仅提供 1 Gbps 的网络接口,那么无论你的 C++ 代码多么优化都无济于事。通往服务器的管道在物理上被堵塞了。合法玩家的流量无法挤入,从而导致近期社区报告中描述的大规模不同步和传送。
缓解 Layer 4 攻击需要基础设施级的防御策略。
DIY 方法
如果你运行自己的专用裸机服务器或标准 EC2 实例,你必须手动构建缓解流水线。这通常涉及:
- 设置反向代理 (Reverse Proxy): 你不能向公众暴露实际的 Unreal Engine 服务器 IP。你必须通过 UDP 代理(如配置了
stream模块用于 UDP 转发的 NGINX 或 HAProxy)来路由流量。这会增加一跳延迟,但它允许你隐藏运行 UE 二进制文件的计算实例的真实 IP。 - 配置 iptables/nftables: 你必须编写严格的防火墙规则,以在内核级别丢弃分段的 UDP 数据包并限制每个 IP 的连接。
- 购买企业级缓解服务: 你必须购买昂贵的企业级路由服务(如 AWS Shield Advanced 或 Cloudflare Magic Transit),以便在恶意流量到达你的数据中心之前对其进行清洗。
自行构建这种弹性的基于代理的架构需要设置 Fleet Manager、负载均衡器 (Load balancers) 和复杂的路由表——这通常需要 4-6 个月的专业 DevOps 工作。对于一个试图发布游戏的 Indie 工作室来说,这是一项巨大的财务和时间消耗。
逃离 DevOps 陷阱
这正是后端即服务 (Backend-as-a-Service) 平台旨在解决的基础设施噩梦。使用 horizOn,这种加固的后端基础设施已经预先配置好了。
无需花费数月时间配置 iptables 和担心容积式 UDP 洪泛,我们的平台会为你管理 Edge network。你的游戏实例被屏蔽在企业级路由层之后,该层会自动识别并在恶意 Layer 4 和 Layer 7 流量到达你的实际 Unreal Engine 服务器线程之前将其丢弃。这意味着你的 Tick rate 保持稳定,你的合法玩家保持连接,你可以专注于编写游戏代码,而不是缓解僵尸网络。
Indie 开发者遭受攻击时的 4 个最佳实践
无论你是构建自己的缓解堆栈还是依赖托管的基础设施,你都必须遵循这些核心安全原则来加固你的游戏:
1. 绝不直接向客户端暴露服务器 IP: 如果玩家只需打开 Wireshark 就能看到你的服务器 IP 地址,攻击者也能看到。你必须使用经纪连接或使用会话票据的 Matchmaking 服务。客户端应通过受保护的转发或代理进行连接。
2. 实施严格的会话验证:
不要让客户端仅凭知道 IP 和端口就进行连接。要求在连接字符串中传递由后端生成的短时加密令牌(如 JWT)。在 PreLogin 中立即通过异步 HTTP 调用验证此令牌。这可以确保攻击者无法通过简单地轮换 IP 地址来绕过你的速率限制,因为他们还需要有效的身份验证账户来生成所需的连接令牌。
3. 限制服务器 Tick Rate:
不要运行未封顶的 NetServerMaxTickRate。将其锁定在一个可预测的值(20、30 或 60 Hz),以确保 CPU 余量可用于应对意外的流量飙升。
4. 监控网络边缘,而不仅仅是引擎:
引擎崩溃日志不会告诉你防火墙层面的丢包情况。你必须在操作系统或虚拟机监控程序级别监控入站带宽 (InBytes) 和包速率。入站 UDP 流量的突然飙升(且与玩家数量增加不对应)是流量攻击的主要指标。
保护你的 Multiplayer 游戏免受恶意行为者的攻击是一场持续的军备竞赛。通过在代码中实施激进的速率限制,加固引擎配置,并利用在边缘丢弃恶意流量的基础设施,你可以确保玩家体验到你设计的游戏——没有 Lag、Rubber-banding 和令人沮丧的崩溃。
准备好停止担心服务器基础设施并专注于构建游戏了吗?免费尝试 horizOn,探索我们富有弹性的后端架构如何让你的游戏在压力下保持在线。
来源: [VERY CRITICAL] Organized DDoS Attacks Causing Server Crashes