Garry's Mod 서버를 DDoS 공격으로부터 방어하기

게시일 읽는 시간 46분

Garry's Mod에서는 게임 트래픽과 서버 조회가 같은 포트 27015를 지나갑니다. 서버에서 실제로 효과를 내는 규칙은 무엇인지, RCON과 Lua 네트워크 이벤트는 어떻게 보호하는지, 그리고 어느 공격 규모부터 서버 앞단 네트워크의 필터링만이 통하는지 정리했습니다.

저녁 8시에 3분 동안 사라졌다가 다시 돌아오는 Garry's Mod 서버는 하드웨어 문제인 경우가 드뭅니다. 대부분은 공격이 진행되는 중이고, 그것도 플레이어가 가장 많이 접속해 있는 바로 그 시간대에 벌어집니다. 그래서 Garry's Mod의 DDoS 방어는 먼저 어떤 패킷이 서버에 도착해도 되는지 아는 일에서 시작합니다. 이 글은 그 순서대로, 앞으로 10분 안에 한 푼도 들이지 않고 직접 막을 수 있는 것, 그 조치가 물리적으로 끝나는 지점, 그리고 그다음 서버 앞단 네트워크에서 무엇이 이뤄져야 하는지를 보여 줍니다.

모든 내용은 Debian 12, Debian 13, Ubuntu 22.04 LTS 또는 Ubuntu 24.04 LTS에서 운영하는 srcds 서버를 기준으로 합니다. 설정 파일은 garrysmod/cfg/server.cfg에 있고, 명령은 root 기준으로 적었으니 일반 사용자라면 앞에 sudo를 붙이세요. 여기서 말하는 것은 언제나 직접 운영하는 루트 서버 또는 Dedicated Server이고, 게임 서버 업체에서 빌린 슬롯이 아닙니다.

공격이 지금 진행 중이라면: 지금 server.cfg를 손대지 마시고 srcds를 재시작하지 마세요. 먼저 측정값을 확보하세요(9번 항목). 공격이 끝나면 그 값은 사라집니다. 재시작은 카운터를 잃게 하고, 그다음 서버를 같은 플러드 속으로 되돌려 놓을 뿐입니다.

Garry's Mod 서버에 DDoS 방어가 필요한 이유

Garry's Mod 서버는 자기 IP 주소와 포트를 스스로 공개합니다. 실수가 아니라 전제입니다. 서버 브라우저에 오르지 않는 서버에는 새 플레이어가 들어오지 않습니다. 서버 브라우저의 이 항목은 서버가 Steam 마스터 서버에 등록하고, 그다음 외부에서 들어오는 모든 A2S 쿼리에 응답하기 때문에 생깁니다. 그래서 질문은 공격자가 여러분의 주소를 찾아내는지가 아니라, 그 주소로 쏘았을 때 무슨 일이 벌어지는지입니다.

여기에 커뮤니티의 성격이 더해집니다. Garry's Mod는 대개 한 판씩 끊어 가며 하는 게임이 아니라 계속 이어지는 세계에서 플레이됩니다. DarkRP 커뮤니티는 플레이어 계정, 소유물, 직업, 진행도를 데이터베이스에 몇 달에 걸쳐 쌓아 둡니다. 그래서 금요일 저녁의 장애는 한 판을 잃는 것보다 비싸고, 고정 플레이어를 잃게 만듭니다. 바로 그래서 경쟁 커뮤니티, 차단된 플레이어, 그리고 돈을 주고 쓰는 서버 booter(월 몇 유로에 임의의 주소로 공격을 걸어 주는 서비스)가 가장 흔한 세 가지 계기입니다. 공격하는 쪽은 실력도 이렇다 할 비용도 들이지 않습니다.

기술적으로는 세 가지 특성이 겹칩니다. 게임 트래픽은 UDP로 흐르고, UDP에는 요구할 수 있는 연결 수립 절차가 없어 출발지 주소를 위조할 수 있습니다. 서버 조회는 게임과 같은 포트에 있으므로 거친 차단은 언제나 양쪽을 함께 때립니다. 그리고 그 위에 Lua가 있습니다. Workshop 애드온은 저마다 자기 코드를 같은 프로세스에 싣고, 보호되지 않은 네트워크 이벤트 하나만 있어도 한 명의 클라이언트가 대역폭 없이 서버를 주저앉힐 수 있습니다. DDoS 공격이 근본적으로 무엇인지는 DDoS 공격이란 무엇인가? 글에서 설명합니다.

Garry's Mod에서 실제로 문제가 되는 포트

Garry's Mod 서버는 기본값으로 포트 27015에서 시작하며, 게임과 서버 조회는 UDP로, RCON은 TCP로 같은 번호를 씁니다. 번호는 시작할 때 -port로 바꾸고, 여러 인스턴스를 운영하면 하나씩 올려 갑니다(27016, 27017 등). 일반적인 시작 명령은 다음과 같습니다.

./srcds_run -game garrysmod -console \
  -port 27015 \
  +maxplayers 64 \
  +gamemode darkrp \
  +map rp_downtown_v4c_v2 \
  +sv_setsteamaccount YOUR_GSLT_TOKEN \
  +host_workshop_collection 123456789 \
  -authkey YOUR_STEAM_WEB_API_KEY

여기에서 전체 공격 표면이 나옵니다. 다음 표가 아래에 나오는 모든 방화벽 규칙의 바탕입니다.

포트와 프로토콜 용도 변경 방법 공개 네트워크에 열어야 하나요
27015/UDP 같은 포트에서 게임 트래픽과 A2S 쿼리 -port 예, 실제로 열려 있어야 하는 유일한 포트
27015/TCP RCON, 곧 Source RCON 프로토콜 -port(게임과 같은 번호) 아니요, 본인 주소만
27005/UDP 클라이언트 포트, 플레이어 쪽에서 나감 -clientport 아니요, 서버에는 규칙이 필요 없음
27020/UDP SourceTV +tv_port 실제로 중계할 때만
26901/UDP Steam 마스터 서버 등록 아웃바운드 아니요, 인바운드 규칙 필요 없음
80/TCP와 443/TCP sv_downloadurl을 통한 FastDL, 웹 서버가 같은 호스트에 있는 경우 웹 서버 FastDL이 그곳에 있을 때만(분리하는 편이 낫습니다)
3306/TCP DarkRP와 플레이어 데이터용 MySQL(mysqloo 모듈 경유) bind-address 아니요, 오직 127.0.0.1
22/TCP SSH 접속 sshd_config 예, 다만 제한해서

이 여덟 줄 가운데 제한 없이 공개 네트워크에 열려야 하는 것은 정확히 하나, 27015/UDP입니다. 나머지는 본인 주소로 제한하거나 127.0.0.1에 바인딩하거나 아예 띄우지 않습니다. 이 주제에서 가장 값비싼 착각은 Garry's Mod에 그냥 닫아 버릴 수 있는 분리된 쿼리 포트가 있다고 여기는 것입니다. 그런 포트는 없습니다.

돈을 쓰기 전에 직접 할 수 있는 일

이 절이 가장 길고, 의도한 바입니다. 설정이 깔끔한 Garry's Mod 서버는 어디에 놓여 있든 작거나 중간 규모의 공격을 자체 힘으로 견뎌 냅니다. 여기 나오는 것은 어느 하나도 돈이 들지 않고, 대부분 15분이면 끝납니다.

1. 현황 파악: 무엇이 대기하고 있나요?

규칙을 한 줄이라도 쓰기 전에 서버가 외부에 무엇을 내주고 있는지 확인하세요. 추측하지 말고 직접 확인합니다.

ss -lntup

중요한 것은 로컬 주소 열입니다. 0.0.0.0:27015와 [::]:27015는 “인터넷 전체에서 접근할 수 있음”을 뜻하고, 127.0.0.1:3306은 “로컬에서만 접근할 수 있음”을 뜻하므로 방화벽 규칙이 필요하지 않습니다. 오래 운영된 DarkRP 서버에서는 거의 언제나 예상보다 많은 서비스가 나옵니다. MySQL, FastDL용 웹 서버, 패널, Discord 봇, 27016에서 돌아가는 두 번째 테스트 서버, 그리고 잊고 방치한 음성 서비스입니다. 공격자의 시선은 외부에서 실행하는 포트 스캔이 보여 줍니다.

nmap -Pn -sU -sT -p- --min-rate 1000 YOUR.SERVER.IP.ADDRESS

2. srcds가 실제로 필요한 포트만 열어 두기

Garry's Mod에는 외부로 향한 허용 하나면 충분하고, 여기에 SSH와 제한된 RCON 접근을 더합니다. UFW에서는 다음과 같이 하며, 스스로 접속이 막히지 않도록 반드시 이 순서를 지키세요.

ufw allow 22/tcp comment 'SSH'
ufw allow 27015/udp comment 'Garrys Mod 게임과 A2S'
ufw allow from 203.0.113.10 to any port 27015 proto tcp comment 'RCON'
ufw default deny incoming
ufw default allow outgoing
ufw --force enable
ufw status verbose

203.0.113.10은 본인 주소로 바꾸세요. 27020/UDP의 SourceTV는 실제로 중계할 때만 여세요. 복구 방법까지 포함한 전체 안내는 스스로 접속이 막히지 않게 UFW 방화벽 설정하기에 있습니다. 그래도 막혔다면, KernelHost의 KVM 루트 서버와 Dedicated Server에는 IPMI도 iDRAC도 없고 고객 포털의 VNC 콘솔로 돌아옵니다. 이 콘솔은 게스트 시스템의 네트워크 스택에 매달려 있지 않으므로, 게스트 안의 방화벽 규칙이 이를 막을 수 없습니다.

데이터베이스는 어떤 경우에도 공개 네트워크에 두어서는 안 됩니다. /etc/mysql/mariadb.conf.d/50-server.cnf에 다음 줄이 있는지 확인하세요.

bind-address = 127.0.0.1

3. 서버 목록에서 사라지지 않으면서 A2S 쿼리 제한하기

가장 많은 Garry's Mod 서버를 잃게 만드는 실수가 여기 있습니다. 게임 트래픽과 서버 조회가 같은 포트를 쓰기 때문에, 27015/UDP를 일괄 차단하거나 너무 좁게 속도 제한하면 자기 플레이어를 내쫓고 공격을 공격자가 원하는 방향으로 끝맺어 줍니다. 올바른 출발점은 조회 패킷과 게임 패킷을 구별하는 것입니다.

엔진은 이를 위해 server.cfg에 들어가는 콘솔 변수 세 개를 제공합니다. 기본값은 보수적이지만, 어쨌든 설정되어 있습니다.

sv_max_queries_sec 3
sv_max_queries_sec_global 60
sv_max_queries_window 30

sv_max_queries_sec는 출발지 주소별로 응답하는 조회 수를 제한하고(기본값 초당 3회), sv_max_queries_sec_global은 모든 주소를 합한 총량을 제한하며(기본값 초당 60회), sv_max_queries_window는 평균을 내는 구간을 정합니다(기본값 30초). 이 값들은 서버가 쓸데없이 응답을 만들어 내는 일로 CPU를 소모하지 않도록 지켜 줍니다. 패킷이 도착하는 것 자체를 막아 주지는 않으며, 전역 값을 아주 좁게 잡으면 목록 페이지의 조회까지 응답을 받지 못해 공격이 진행되는 동안 서버 브라우저에서 사라집니다.

한 단계 아래에서는 조회 트래픽을 깔끔하게 분리할 수 있습니다. Source 엔진의 연결 없는 패킷은 모두 네 바이트가 전부 1로 채워진 값(0xffffffff)으로 시작하고, 이미 접속한 플레이어의 트래픽에는 이 머리가 없습니다. 바로 이 지점에 nftables로 출발지 주소별 속도 제한을 걸 수 있습니다.

table inet gmod {
    chain input {
        type filter hook input priority -10; policy accept;
        udp dport 27015 @th,64,32 0xffffffff \
            meter a2sflood { ip saddr limit rate over 8/second burst 20 packets } drop
    }
}

이 파일은 nft -f로 불러옵니다. 우선순위 -10은 이 규칙이 UFW의 필터 체인보다 먼저 동작하게 하고, @th,64,32는 UDP 헤더 뒤의 첫 네 바이트를 읽습니다. 고전적인 iptables에서는 u32 매칭이 같은 일을 합니다.

iptables -A INPUT -p udp --dport 27015 \
  -m u32 --u32 "0>>22&0x3C@8=0xFFFFFFFF" \
  -m hashlimit --hashlimit-name gmod_a2s --hashlimit-mode srcip \
  --hashlimit-above 8/sec --hashlimit-burst 20 -j DROP

인터넷의 안내서가 거의 빠뜨리는 점이 하나 있습니다. 연결이 없는 것은 서버 조회만이 아니라 연결 수립도 마찬가지입니다. 새로 들어오는 플레이어는 게임에 들어서기 전에 같은 머리를 가진 패킷을 여러 개 보냅니다. 그래서 한도를 너무 좁게 잡으면 서버는 계속 닿을 수 있는데도 새 플레이어가 막힙니다. 넉넉하게 시작하고(주소별로 초당 8에서 15개), 평상시 운영을 한 주 측정한 뒤에 비로소 한도를 좁히세요.

4. RCON을 보호하거나 완전히 끄기

RCON은 Source 서버에서 인기 있는 표적이고, 이유가 세 가지 동시에 있습니다. 첫째, 게임과 같은 포트 번호에 있고 프로토콜만 TCP이므로 찾는 수고 없이 발견됩니다. 둘째, Source RCON 프로토콜은 비밀번호를 평문으로, TLS도 키 교환도 없이 전송합니다. 트래픽을 들여다보는 사람은 그대로 손에 넣습니다. 셋째, 이득이 최대입니다. RCON을 가진 사람은 맵을 바꾸고, 모든 플레이어를 차단하고, 설정을 바꾸고, 서버를 멈출 수 있습니다. RCON을 탈취한 공격자에게는 대역폭이 더 이상 필요하지 않습니다.

rcon_password를 비운 채로도, 추측할 수 있는 값으로도 두지 마세요. openssl rand -base64 32의 결과면 충분합니다. 엔진은 로그인 시도에 맞서는 제동 장치도 함께 제공합니다.

rcon_password "A_LONG_RANDOM_PASSWORD_HERE"
sv_rcon_minfailures 3
sv_rcon_maxfailures 5
sv_rcon_minfailuretime 30
sv_rcon_banpenalty 1440

이렇게 하면 30초 안에 세 번 실패한 주소가 하루 동안 차단됩니다. 여기에 경고가 둘 있습니다. 첫째, 바로 이 장치가 여러분의 관리자 패널까지 차단합니다. 그곳에 옛 비밀번호가 저장되어 있다면 그렇습니다. 운영자가 “RCON이 갑자기 안 된다”고 알려 오는 상황은 대개 자기 차단입니다. 둘째, 2번 항목의 방화벽 제한이 더 효과적입니다. 시도 자체가 애플리케이션까지 닿지 못하게 막기 때문입니다. RCON을 어쩌다 한 번만 쓴다면 포트를 완전히 닫고 SSH 포워딩으로 작업하세요.

ssh -N -L 27015:127.0.0.1:27015 root@YOUR.SERVER.IP.ADDRESS

5. Lua 네트워크 메시지 제한하기, 가장 흔한 자체 유발 장애

DDoS로 신고되는 Garry's Mod 장애 가운데 상당 부분은 DDoS가 아닙니다. 초당 몇 킬로비트만 쓰는 접속자 한 명이 일으키는 Lua 과부하입니다. 원인은 net 라이브러리의 구조에 있습니다. 애드온이 util.AddNetworkString으로 네트워크 이벤트를 등록하고 net.Receive로 그것을 듣기 시작하면, 어떤 클라이언트든 그 이벤트를 반복문으로 일으킬 수 있습니다. 자체 제한이 없으면 서버는 메시지 하나하나를 전부 실행합니다. Facepunch는 이를 자체 오류 보고에서 여러 번 기록했고 엔진에는 해결책을 두지 않았습니다. 제한은 명시적으로 애드온 작성자의 몫입니다.

그러니 직접 만든 애드온과 사 온 애드온을 모두 세 가지 관점에서 점검하세요. 플레이어별 초당 상한, 메시지 길이 검사, 그리고 플레이어를 메시지 내용이 아니라 서버 쪽에서 두 번째 파라미터로 확인하는지입니다. 쓸 만한 형태는 다음과 같습니다.

util.AddNetworkString("khrp_buy")

local budget = {}

net.Receive("khrp_buy", function(len, ply)
    if not IsValid(ply) then return end
    if len > 256 then return end

    local now = CurTime()
    local b = budget[ply]

    if not b or now - b.start >= 1 then
        b = { start = now, count = 0 }
        budget[ply] = b
    end

    b.count = b.count + 1
    if b.count > 10 then return end

    KHRP.HandleBuy(ply, net.ReadString())
end)

hook.Add("PlayerDisconnected", "khrp_budget_cleanup", function(ply)
    budget[ply] = nil
end)

여기에 server.cfg의 두 줄이 딸립니다. sv_allowcslua는 Garry's Mod에서 기본값이 1이고 클라이언트가 lua_run_cl과 lua_openscript_cl로 자기 코드를 실행하도록 허용합니다. 공개 서버라면 이 값은 0이어야 합니다. 그리고 sv_kickerrornum은 지정한 수보다 많은 클라이언트 측 오류를 만드는 클라이언트를 내보냅니다(기본값 0, 곧 꺼진 상태).

sv_allowcslua 0
sv_kickerrornum 25

6. Workshop 콘텐츠와 FastDL을 게임 서버에서 분리하기

Garry's Mod에서 Workshop 애드온은 곁가지가 아니라 일상입니다. DarkRP 커뮤니티는 +host_workshop_collection으로 자기 컬렉션을 붙이고, 클라이언트는 그 콘텐츠를 Steam에서 직접 받습니다. 여러분의 회선에는 부담이 되지 않습니다. -authkey의 키는 Steam 웹 API 키이고 비밀번호처럼 다루어야 합니다. 시작 스크립트에는 넣고, 공개 저장소나 Discord 채널에는 넣지 마세요.

대역폭이 드는 쪽은 두 번째 경로입니다. Workshop에서 오지 않는 모든 것(직접 만든 맵, 사운드, 머티리얼)은 다운로드 경로로 갑니다. sv_downloadurl이 없으면 이 경로는 게임 포트 자체를 지나가고 게임 트래픽과 직접 경쟁합니다. FastDL을 쓰면 HTTP로 갑니다. 이 웹 서버가 같은 호스트와 같은 IP 주소에 있으면 둘이 같은 회선을 나눠 쓰게 되므로, 접속이 몰리는 상황이나 80/TCP를 겨냥한 공격이 게임까지 때립니다. 다음 값이 적절합니다.

sv_downloadurl "https://fastdl.your-domain.com/garrysmod/"
sv_allowdownload 1
sv_allowupload 0
net_maxfilesize 64

sv_allowupload 0은 클라이언트가 서버로 파일을 보낼 가능성을 없애고, 필요하지도 통제되지도 않는 경로 하나를 닫습니다. net_maxfilesize는 게임 연결로 전송하는 파일의 크기를 메가바이트 단위로 제한합니다. 가능하다면 FastDL을 다른 호스트나 별도의 이름 뒤에 두세요. 그러면 부하가 게임 포트와 같은 주소에 얹히지 않습니다.

7. 접속 플러드와 슬롯 고갈 막아 내기

슬롯 고갈은 대역폭이 필요하지 않은 공격입니다. 공격자가 자동화된 연결로 빈자리를 모두 차지해서 실제 플레이어가 꽉 찬 서버를 보게 만듭니다. Garry's Mod에서는 접속 하나하나가 서버에 일을 만든다는 점이 더 불리하게 작용합니다. 플레이어가 게임에 들어서기 훨씬 전에 리소스 목록과 게임모드를 주고받기 때문입니다.

이에 맞서는 것은 네 가지입니다. 첫째, 현실적인 상한입니다. +maxplayers를 게임모드가 감당하는 수보다 높게 잡으면 공격 표면만 커집니다. 둘째, sv_timeout입니다. 메시지가 없는 상태가 몇 초 이어지면 클라이언트를 끊을지 정하며(널리 쓰이는 설정에서는 120), 걸려 있는 미완성 연결을 더 빨리 치우고 싶으면 값을 낮춥니다. 셋째, 3번 항목의 연결 없는 패킷에 대한 속도 제한입니다. 연결 수립이 바로 그곳을 지나가기 때문입니다. 넷째, 닫힌 모임을 위한 서버 비밀번호입니다.

sv_password "regulars_2026"
sv_timeout 90
sv_filterban 1
sv_region 3

진짜 허용 목록은 Garry's Mod가 자체로 제공하지 않고, ULX 같은 확장 기능이나 CheckPassword 훅에서의 자체 검사로 만듭니다. 그리고 한 가지는 분명해야 합니다. 허용 목록은 여러분의 게임 로직을 지키고 회선은 지키지 않습니다. 서버를 플러딩하는 공격자는 애초에 들어올 생각이 없습니다. 그 패킷은 거부되지만 그래도 이미 도착했고, 바로 그 점이 핵심입니다.

8. 커널 부담 덜기: 연결 추적과 수신 버퍼

이 항목은 볼륨 공격처럼 보이지만 실은 아닌 장애를 설명해 줍니다. 커널은 UDP 트래픽에 대해서도 연결 추적(conntrack)에 항목을 만들고, 출발지 주소가 위조되어 있으면 주소마다 새 항목이 생깁니다. 테이블이 가득 차면 커널은 구별 없이 패킷을 폐기합니다. 공격과 여러분의 플레이어가 함께 튕겨 나가고, 시스템 로그에는 “nf_conntrack: table full”이 남습니다. 현재 값과 상한은 다음 명령으로 확인합니다.

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max

가장 효과적인 조치는 게임 트래픽을 애초에 추적하지 않게 하는 것입니다. 엔진이 자기 세션을 스스로 관리하기 때문입니다.

table inet raw {
    chain prerouting {
        type filter hook prerouting priority raw; policy accept;
        udp dport { 27015, 27020 } notrack
    }
    chain output {
        type filter hook output priority raw; policy accept;
        udp sport { 27015, 27020 } notrack
    }
}

iptables에서는 iptables -t raw -A PREROUTING -p udp --dport 27015 -j NOTRACK이 이에 해당하고, OUTPUT에는 같은 줄을 --sport로 씁니다. 그 뒤로 이 포트에는 명시적인 허용이 필요합니다. 추적이 없으면 기존 상태를 확인하는 규칙은 더 이상 동작하지 않기 때문입니다. 또한 srcds가 가져가는 속도보다 패킷이 빨리 도착하면 수신 버퍼가 넘치고, 플레이어에게는 비어 있는 회선에서 패킷 손실이 일어난 것처럼 보입니다.

net.core.rmem_max = 16777216
net.core.rmem_default = 1048576
net.core.netdev_max_backlog = 16384

이 줄들은 /etc/sysctl.d/ 아래의 파일에 넣고 sysctl --system으로 적용합니다. 정말 필요한지는 커널이 직접 알려 줍니다. nstat -az의 UdpRcvbufErrors가 올라간다면 값이 효과를 냅니다. 카운터가 0에 머물러 있으면 이 조정은 아무것도 바꾸지 않습니다. 이것은 여유분이고 방어가 아닙니다.

9. 모든 것이 정상인 동안 측정값 확보하기

가장 중요하면서 거의 아무도 미리 하지 않는 단계가 있습니다. 기준값을 만들어 두는 것입니다. 평상시 값이 없으면 사건이 끝난 뒤에 초당 40,000 패킷이 많았던 것인지 그냥 토요일 저녁이었던 것인지 말할 수 없습니다. 여러분의 서버에 맞는 평상시 값을 한 번 계산해 두세요. cl_cmdrate 66으로 플레이하는 64명은 초당 약 4,200개의 수신 패킷을 만들고, 그보다 확실히 많으면 설명이 필요합니다. apt-get install -y vnstat sysstat로 측정이 상시 돌아갑니다. 사건이 진행되는 동안에는 명령 네 개로 충분합니다.

sar -n DEV 1 10
ip -s link show eth0
nstat -az | grep -i udp
tcpdump -ni eth0 -c 200 "udp port 27015 and udp[8:4] = 0xffffffff"

첫 번째 명령은 초당 패킷과 바이트를, 두 번째는 인터페이스의 폐기 카운터를, 세 번째는 커널의 UDP 오류 카운터를 보여 줍니다. 네 번째 줄은 연결 없는 패킷만, 곧 조회 플러드가 악용하는 바로 그 부류만 보여 줍니다. 접속한 사람이 거의 없는데 카운터가 몇 초 만에 다 차면 답을 얻은 것입니다. tcpdump는 항상 -c로 개수를 제한하세요. 과부하 상태에서의 캡처는 이미 과부하인 서버에 부담을 더 얹습니다. 측정값을 해석하는 방법은 서버에서 DDoS 공격 알아내기에 있습니다.

A2S 반사 공격 취약점은 무엇이고 아직 나와 상관이 있나요

A2S 반사 공격은 여러분의 서버가 표적이 아니라 도구가 되는 공격입니다. 공격자가 출발지 주소를 위조한 작은 조회를 수천 대의 게임 서버에 보내면, 그보다 훨씬 큰 응답이 모두 실제 피해자에게 쏟아집니다. 역사적으로 A2S_INFO 요청은 25 바이트였고(4 바이트 0xFFFFFFFF, 1 바이트 0x54, 그리고 문자열 “Source Engine Query”를 위한 20 바이트), 응답은 그와 달리 수백 바이트였습니다. US-CERT는 증폭 공격 목록에서 Steam 프로토콜의 계수를 5.5로 제시하며, 이는 공격자 쪽의 1 기가비트가 피해자 쪽에서 5.5 기가비트가 된다는 뜻입니다.

Valve는 2020년 11월부터 이 취약점을 두 가지 방법으로 막았습니다. 그 뒤로 연결 없는 조회 패킷은 보내는 쪽에서 1,200 바이트로 채워야 하며, 그러면 요청이 응답보다 커지고 증폭 계수가 1 아래로 떨어집니다. 전환 기간에는 운영자가 환경 변수 STEAM_GAMESERVER_MIN_CONNECTIONLESS_PACKET_SIZE=1200으로 더 엄격한 동작을 미리 강제할 수 있었습니다. 여기에 더해 서버는 A2S_PLAYER와 A2S_RULES에 곧바로 데이터로 응답하지 않고 선행 요구 값(S2C_CHALLENGE)으로 응답하며, 조회하는 쪽은 두 번째 요청에서 이 값을 되돌려 보내야 합니다. 출발지 주소를 위조하는 쪽은 이 값을 아예 보지 못합니다.

여기에서 여러분에게 따라오는 것은 두 가지입니다. 서버 바이너리를 최신으로 유지하세요. 이 보호는 여러분의 설정이 아니라 Steam 게임 서버 기반에 들어 있습니다. 그리고 반사 공격을 여러분 자신을 겨냥한 조회 플러드와 혼동하지 마세요. 두 번째 형태에는 3번 항목의 속도 제한만이, 그리고 그 위로는 서버 앞단 네트워크의 필터링만이 통합니다.

이 조치들이 한계에 이르는 지점: 대역폭과 패킷 전송률

이제 어떤 설정 파일로도 해결할 수 없는 부분입니다. 지금까지 설명한 모든 것은 여러분의 서버, 곧 회선의 끝에서 동작합니다. 방화벽 규칙은 이미 케이블을 지나온 패킷을 두고 판단합니다. 그 패킷을 폐기할 수는 있지만, 보내지 않은 것으로 만들 수는 없습니다.

한번 계산해 보겠습니다. 일반적인 게임 서버는 1 Gbit/s 회선에 물려 있고, 이는 초당 125 메가바이트이며, 누군가 그보다 많이 보내는 순간 회선은 꽉 찹니다. 두 번째 수치가 보통 먼저 터집니다. 가장 작은 64 바이트 패킷이라면 1 Gbit/s에 초당 약 149만 개, 10 Gbit/s에 약 1,488만 개가 들어갑니다. 일반적인 서버 커널은 CPU와 네트워크 카드에 따라 그중 수십만 개를 처리한 뒤 폐기를 시작합니다. 그래서 회선을 3분의 1도 채우지 못하는 공격이 서버를 멈춰 세울 수 있습니다. 폐기에 연산 시간이 다 들어가기 때문입니다. 운영자는 이것을 “사용률은 전혀 높지 않았는데 그래도 전부 랙 스파이크를 겪었다”로 경험합니다.

항목 값
A2S_INFO 요청, 역사적 크기 25 바이트
Steam 프로토콜의 증폭 계수(US-CERT) 5.5
2020년 이후 연결 없는 조회 패킷의 최소 크기 1,200 바이트
평상시 트래픽: cmdrate 66으로 플레이하는 64명 초당 약 4,200개의 수신 패킷
64 바이트 패킷일 때 1 Gbit/s 초당 약 149만 개(초당 125 메가바이트)
64 바이트 패킷일 때 10 Gbit/s 초당 약 1,488만 개
커뮤니티 게임 서버를 겨냥한 일반적인 공격 규모 5에서 50 Gbit/s
KernelHost 서버에서 측정된 최대치 초당 4,150만 패킷과 함께 473.4 Gbit/s

어떤 규모가 실제로 나타나는지 가늠해 보겠습니다. KernelHost 서버에서는 그중에서도 게임 서버를 겨냥해 112.2 Gbit/s를 넘고 초당 870만 패킷을 넘는 UDP 플러드가, 그리고 음성 서버를 겨냥해 473.4 Gbit/s를 넘고 초당 4,150만 패킷을 넘는 멀티 벡터 공격이 걸러졌습니다. 473.4 Gbit/s는 1 Gbit/s 회선의 약 470배이고, 10 Gbit/s 회선으로 봐도 여전히 약 47배입니다. 이에 맞설 로컬 설정은 존재하지 않습니다. 볼류메트릭 공격은 서버 앞단 네트워크에서 끝나야 합니다.

KernelHost가 이에 맞서 제공하는 것

모든 서버 상품에 포함된 상시 방어

KernelHost의 DDoS 방어는 두 단계로 구성되어 상시 동작하며, 여러분이 무엇을 켜거나 주문하거나 설정할 필요가 없습니다.

  • 1단계: 글로벌 스크러빙 네트워크의 17 Tbps 방어 용량. 볼류메트릭 공격은 발생지에 가까운 곳에서 정화되어 데이터센터에 닿기 전에 걸러집니다.
  • 2단계: 프랑크푸르트암마인의 3.2 Tbps Arbor 실시간 필터링. 서버 바로 앞에서 프로토콜별 패턴을 찾아내 패킷 하나하나를 폐기합니다.

두 가지 특성이 결정적입니다. 이 방어는 상시 동작하므로 공격에 반응해서 비로소 시작될 필요가 없습니다. 그래서 플레이어가 튕겨 나가는 전환 시간이 존재하지 않습니다. 그리고 null-routing을 쓰지 않습니다. 여러분의 IP 주소는 네트워크에 그대로 남고, 폐기되는 것은 악성 패킷뿐입니다. IP 주소를 네트워크에서 빼 버리는 쪽은 여러분에게 공격자와 같은 결과를 안깁니다. 위치는 프랑크푸르트암마인입니다. 어떤 게임과 프로토콜이 포함되는지는 실시간 게임 서버 DDoS 방어에 정리되어 있습니다.

계속 공격받는 커뮤니티를 위한 Advanced DDoS Protection

어떤 프로젝트는 어쩌다 한두 번이 아니라 표적이 되어 몇 주에 걸쳐, 바뀌는 패턴으로, 그것도 늘 사람이 가장 많은 시간에 맞춰 공격받습니다. 이런 경우를 위해 Advanced DDoS Protection이 월 50.00 EUR부터, PrePaid 방식으로, 최소 이용 기간 없이 제공됩니다. 차이는 용량이 더 커지는 데 있지 않고 통제권에 있습니다.

  • 전용 방어 IP: 프랑크푸르트 코어에서 발급되며, 여러분의 서버는 자체 네트워크에서 이 IP로 전환됩니다. 여러분 쪽에서 손볼 것은 없습니다.
  • 포트와 프로토콜별로 직접 관리하는 방어 규칙: 고객 포털에서 27015/UDP에 무엇을 허용할지와 27015/TCP에 무엇을 허용할지를 따로 설정하며, 이를 위해 티켓을 쓸 필요가 없습니다.
  • 변경은 실시간으로 반영되므로, 다음 유지보수 시간을 기다리지 않고 공격이 진행되는 중에도 값을 조정할 수 있습니다.
  • 게임에 맞춘 방어 프로필: Garry's Mod와 나머지 Source 타이틀은 물론, 개조된 서버와 자체 애플리케이션을 위한 자유 TCP 및 UDP 프로필도 함께 제공됩니다.

여기에도 PrePaid 모델이 적용됩니다. 최소 이용 기간도, 해지 통보 기간도, 계약도, 설치비도 없습니다. 공격의 물결이 지나가면 그냥 연장하지 않으시면 됩니다. Garry's Mod 서버를 지금까지 다른 곳에서 운영해 왔다면 KernelHost로 옮겨서 이 방어를 받게 됩니다. 필터링은 자체 네트워크에서 이뤄지고 남의 인프라에서는 이뤄지지 않기 때문입니다.

두 방어 단계 비교

항목 포함된 DDoS 상시 방어 Advanced DDoS Protection
요금 모든 서버 상품에 추가 요금 없이 포함 월 50.00 EUR부터, PrePaid
활성화 서버 제공 시점부터 동작, 설정할 것 없음 주문하고 방어 IP를 받으면 서버가 전환됨
필터링 용량 17 Tbps 글로벌 스크러빙과 프랑크푸르트암마인의 3.2 Tbps Arbor 실시간 필터링 동일한 2단계 필터링
IP 주소 서버의 IP 주소 추가로 받는 전용 방어 IP
규칙 세트 자동 프로필, 설정 불필요 고객 포털에서 포트와 프로토콜별 자체 규칙
변경 자동으로 함께 적용 실시간 반영, 공격 중에도 가능
게임 프로필 주요 게임에 최적화된 프로필, Garry's Mod 포함 포트별로 프로필 선택, 개조된 서버도 가능
공격 중 null-routing 없음 없음
이용 기간 서버 상품에 연동 PrePaid, 최소 이용 기간 없음, 해지 통보 기간 없음, 설치비 없음

대부분의 Garry's Mod 커뮤니티에는 포함된 상시 방어와 깔끔한 서버 설정이면 충분합니다. Advanced DDoS Protection은 누군가 이 일을 개인적으로 받아들였을 때의 답입니다.

자주 나오는 실수와 해결 방법

“서버는 돌아가는데 서버 브라우저에서 사라졌습니다”: 대개 27015/UDP를 일괄 차단했거나 속도 제한을 너무 좁게 잡은 경우입니다. 게임 트래픽과 조회가 같은 포트를 나눠 쓰므로 거친 규칙은 양쪽을 함께 때립니다. 대신 연결 없는 패킷에 대한 매칭으로 작업하세요. 포트에 닿을 수 있는데도 서버가 보이지 않는다면 sv_setsteamaccount를 확인하세요. 유효한 Game Server Login Token이 없으면 Garry's Mod 서버는 목록에서 크게 낮춰 취급되고, 서버마다 자기 토큰이 따로 필요합니다.

“iptables 규칙이 올바른데도 효과가 없습니다”: 흔한 원인이 세 가지입니다. 규칙이 UFW 체인 뒤에 있어 전혀 도달하지 못하거나, 마지막 재시작 이후 사라졌거나(이때는 apt-get install -y iptables-persistent와 netfilter-persistent save 또는 /etc/ufw/before.rules 항목이 도움이 됩니다), 공격이 볼류메트릭이어서 규칙은 이미 꽉 찬 회선에서 제대로 동작하고 있는 경우입니다. iptables -L INPUT -n -v로 매칭 카운터가 올라가는지 확인하세요. 0에 머물러 있으면 규칙에 도달하지 못하는 것입니다.

“DarkRP 서버가 모두에게 버벅이는데 회선은 비어 있습니다”: 이것은 거의 언제나 Lua이고 회선을 겨냥한 공격이 아닙니다. 서버 로그에서 어떤 네트워크 이벤트가 유난히 자주 도착하는지 보고, 해당 애드온에 플레이어별 제한이 있는지 확인하세요. sar -n DEV 1 10과 폐기 카운터가 평범하다면 DDoS 공격이 아니었습니다.

“RCON이 갑자기 안 됩니다”: DDoS가 아니라 대개 자기 차단입니다. 옛 비밀번호가 저장된 관리자 패널이 sv_rcon_minfailures를 발동시키고, sv_rcon_banpenalty가 설정된 분 수만큼 그 주소를 차단합니다. 비밀번호를 고치고 차단을 풀고, 그다음 포트를 본인 주소로 제한하세요.

“IP 주소를 바꿨는데 이틀 뒤에 다시 오프라인이 되었습니다”: 그것이 정상입니다. 서버는 마스터 서버에 다시 등록되는 순간 새 주소를 스스로 공개하고, 공개 주소가 없는 게임 서버에는 플레이어가 없습니다. 주소 변경은 몇 시간에서 며칠을 벌어 주지만 해결책은 아닙니다.

“기존 업체가 제 IP 주소를 차단했습니다”: 그것이 null-routing입니다. 업체는 그렇게 자기 네트워크를 지키지만, 여러분에게 남는 결과는 공격이 성공한 것과 똑같고, 보통 그 뒤로 몇 시간 더 이어집니다. 확실하지 않으면 필터링을 하는지 null-routing을 하는지 물어보세요. 그 답이 어떤 하드웨어 사양보다 여러분의 가용성을 더 크게 좌우합니다.

“tcpdump에 이상한 것이 보이지 않습니다”: 트래픽이 이미 앞단 네트워크에서 걸러지고 있으면 서버에는 아무것도 도착하지 않는 것이 당연합니다. 필터링이 작동하고 있을 때의 정상적인 모습입니다. 반대로 회선이 포화되면 측정에 쓰려던 SSH 세션조차 닿지 않을 수 있습니다. 그럴 때는 고객 포털의 VNC 콘솔을 이용하세요.

핵심 요약

  • Garry's Mod 서버에 필요한 열린 포트는 정확히 하나, 27015/UDP입니다. 게임 트래픽과 A2S 쿼리가 그곳을 함께 쓰며, 분리된 쿼리 포트는 없습니다.
  • RCON은 27015/TCP에 있고 비밀번호를 평문으로 전송하므로, 본인 주소에만 허용하거나 SSH 포워딩으로만 접근해야 합니다.
  • 포트를 제한하지 말고 0xffffffff 머리를 가진 연결 없는 패킷을 제한하세요. 27015/UDP를 일괄 차단하면 자기 플레이어가 튕겨 나갑니다.
  • 가장 흔한 Garry's Mod 장애는 DDoS 공격이 아니라 제한이 없는 네트워크 이벤트입니다. util.AddNetworkString으로 등록한 이벤트에는 모두 플레이어별 초당 상한이 필요합니다.
  • 64 바이트 패킷이라면 1 Gbit/s 회선은 초당 약 149만 개를 나릅니다. 그보다 많아지면 손실은 그 앞의 라우터에서 생기고, 로컬 규칙은 모두 효과를 잃습니다.
  • KernelHost에서는 2단계 상시 방어가 모든 서버 상품에 추가 요금 없이 포함됩니다. 글로벌 스크러빙 네트워크의 17 Tbps 방어 용량과 프랑크푸르트암마인의 3.2 Tbps Arbor 실시간 필터링이며, null-routing은 쓰지 않습니다.
  • 표적이 되어 계속 공격받는다면 월 50.00 EUR부터의 Advanced DDoS Protection을 더하세요. 전용 방어 IP와 포트 및 프로토콜별로 직접 관리하는 규칙이 실시간으로 동작합니다.

서버가 이미 KernelHost에 있다면 필터링은 여러분이 아무것도 하지 않아도 동작하고 있습니다. 그래도 이상한 점이 보이면 지원 티켓을 열어 주세요. 해당 IP 주소의 필터 규칙을 다시 맞춰 드립니다. 그때 네 가지를 함께 알려 주세요. IP 주소, 포트, 여러분 시간대 기준의 발생 구간, 그리고 보이는 현상(플레이어가 튕겨 나감, 서버가 브라우저에 없음, 랙 스파이크)입니다. 공격이 진행 중일 때는 WhatsApp 긴급 채팅 +43 650 8209883으로도 연락하실 수 있습니다.

Garry's Mod 외에 다른 Source 타이틀도 운영한다면 공통 기초는 CS2와 Source 서버를 DDoS 공격으로부터 방어하기에 있고, 기반을 깔끔하게 구축하는 방법은 SteamCMD로 게임 서버 설치하기에서 다룹니다.

자주 묻는 질문

Garry's Mod 서버가 지금 오프라인입니다. DDoS 공격인가요?
먼저 CPU 부하가 아니라 패킷 전송률을 확인하세요. sar -n DEV 1 10은 초당 패킷과 바이트를, ip -s link show eth0은 인터페이스의 폐기 카운터를, nstat -az는 커널의 UDP 오류 카운터를 보여 줍니다. 접속한 사람이 거의 없는데도 수신 패킷이 평상시 값을 크게 웃돌면 공격입니다. 네트워크 카운터가 평범한데도 모든 것이 버벅인다면 원인은 거의 언제나 Lua입니다. 그때는 애드온이나 보호되지 않은 네트워크 이벤트가 연산 시간을 먹는 것이고, 세상의 어떤 필터링도 그것을 바꾸지 못합니다.
Garry's Mod 서버에 실제로 필요한 포트는 무엇인가요?
정확히 하나, 27015/UDP이고 시작 파라미터 -port로 정합니다. 이 하나의 포트로 게임 트래픽과 서버 브라우저의 A2S 쿼리가 함께 지나가며, Garry's Mod에는 분리된 쿼리 포트가 없습니다. RCON은 27015/TCP에 있고 오직 본인 주소에만 열어야 합니다. 27020/UDP는 SourceTV로 중계할 때만 필요합니다. 클라이언트 포트 27005/UDP는 플레이어 쪽에서 나가므로 서버에는 규칙이 필요하지 않습니다. DarkRP용 MySQL은 127.0.0.1에 바인딩해야 하고 공개 네트워크에 두어서는 안 됩니다.
조회 플러드를 멈추려고 쿼리 포트를 차단해도 되나요?
안 됩니다. 분리된 쿼리 포트가 아예 없기 때문입니다. 27015/UDP를 차단하거나 일괄 속도 제한하는 운영자는 같은 동작으로 자기 플레이어를 내쫓고 서버 브라우저에서도 사라집니다. 올바른 방법은 연결 없는 패킷만 겨냥한 제한입니다. Source 엔진의 모든 서버 조회와 연결 수립은 네 바이트 0xffffffff로 시작하고, 이미 접속한 플레이어의 트래픽에는 이 머리가 없습니다. 바로 이 패턴에 nftables나 iptables로 출발지 주소별 한도를 걸고, 시작값은 초당 8개 정도로 잡으세요.
Garry's Mod에서 RCON이 그렇게 인기 있는 표적인 이유는 무엇인가요?
이득은 최대이고 문턱은 낮기 때문입니다. RCON은 게임과 같은 포트 번호에 있고 프로토콜만 TCP이므로 찾는 수고 없이 발견됩니다. Source RCON 프로토콜은 비밀번호를 평문으로, TLS도 키 교환도 없이 전송합니다. 그리고 RCON을 탈취한 사람은 대역폭 한 방울 없이 맵을 바꾸고, 모든 플레이어를 차단하고, 설정을 바꾸고, 서버를 멈출 수 있습니다. 그러니 길고 무작위한 비밀번호를 쓰고, sv_rcon_minfailures와 sv_rcon_banpenalty를 켜고, 27015/TCP는 본인 주소에만 열어 두세요.
A2S 반사 공격 취약점은 무엇이고 아직 나와 상관이 있나요?
A2S 반사 공격은 여러분의 서버가 표적이 아니라 도구가 되는 공격입니다. 공격자가 출발지 주소를 위조해 수천 대의 게임 서버를 조회하면, 그보다 훨씬 큰 응답이 실제 피해자에게 모여듭니다. A2S_INFO 요청은 역사적으로 25 바이트였고, US-CERT는 Steam 프로토콜의 증폭 계수를 5.5로 제시합니다. Valve는 2020년 11월부터 이 취약점을 막았습니다. 조회 패킷은 1,200 바이트로 채워야 하고, A2S_PLAYER와 A2S_RULES는 선행 요구 값을 요구합니다. 서버 바이너리를 최신으로 유지하면 이 보호가 동작합니다.
공격이 진행되는 동안 제 방화벽 규칙이 아무 효과도 내지 못하는 이유는 무엇인가요?
패킷이 이미 도착한 뒤에야 동작하기 때문입니다. 1 Gbit/s 회선은 64 바이트 패킷이라면 초당 약 149만 개를, 10 Gbit/s 회선은 약 1,488만 개를 나릅니다. 공격이 그보다 크면 손실은 그 앞의 라우터에서 생기고 여러분의 규칙은 아예 실행되지 않습니다. 게다가 회선이 꽉 차기 훨씬 전에 CPU가 먼저 한계에 이릅니다. 패킷 하나하나가 나중에 폐기되더라도 네트워크 스택을 한 번 통과하는 비용을 치르게 하기 때문입니다. 이 지점부터는 서버 앞단 네트워크의 필터링만이 통합니다.
DarkRP 서버에 랙 스파이크가 생기는데 회선은 비어 있습니다. 원인이 무엇인가요?
그렇다면 거의 언제나 Lua이고 회선을 겨냥한 공격이 아닙니다. 애드온이 util.AddNetworkString으로 네트워크 이벤트를 등록하고 net.Receive로 그것을 듣기 시작하면, 접속한 어떤 클라이언트든 그 이벤트를 반복문으로 일으킬 수 있고 서버는 메시지 하나하나를 전부 실행합니다. 초당 몇 킬로비트를 쓰는 플레이어 한 명으로 충분합니다. 해결책은 방화벽이 아니라 애드온에 있습니다. 플레이어별 초당 상한, 메시지 길이 검사, 그리고 플레이어를 메시지 내용이 아니라 서버 쪽에서 확인하는 것입니다.
KernelHost의 서버는 공격 중에 오프라인이 되나요?
아니요. null-routing을 쓰지 않습니다. 여러분의 IP 주소는 네트워크에 그대로 남고, 폐기되는 것은 악성 패킷뿐입니다. 방어는 두 단계로 구성되어 있습니다. 글로벌 스크러빙 네트워크의 17 Tbps 방어 용량과 프랑크푸르트암마인의 3.2 Tbps Arbor 실시간 필터링입니다. 이 방어는 상시 동작하며 공격에 먼저 반응할 필요가 없으므로, 여러분의 플레이어가 튕겨 나가는 전환 시간이 존재하지 않습니다. 이 상시 방어는 모든 서버 상품에 추가 요금 없이 포함되어 있고 서버 제공 시점부터 동작합니다.
Advanced DDoS Protection이 추가로 필요한 때는 언제인가요?
커뮤니티가 어쩌다 한두 번이 아니라 표적이 되어 몇 주에 걸쳐 공격받고, 필터링을 직접 조정하려 할 때입니다. 전용 방어 IP를 받고, 고객 포털에서 포트와 프로토콜별 방어 규칙을 직접 관리하므로 27015/UDP와 27015/TCP를 서로 다르게 설정할 수 있습니다. 변경은 실시간으로 반영되므로 공격이 진행되는 중에도 값을 조정할 수 있습니다. 요금은 월 50.00 EUR부터, PrePaid, 최소 이용 기간 없음, 해지 통보 기간 없음, 설치비 없음입니다.

Garry's Mod Garry's Mod DDoS 방어 DarkRP Source 엔진 A2S 쿼리 게임 서버 보호 포트 27015 Advanced DDoS Protection