Как определить DDoS-атаку: точная диагностика на сервере
Не всякая перегрузка это атака. Как с помощью ss, счётчиков пакетов, сообщений ядра и логов веб-сервера безошибочно отличить DDoS-атаку от всплеска нагрузки и программной ошибки.
Сервис перестал отвечать, индикатор нагрузки упёрся в потолок, а в чате уже висит вопрос: нас атакуют? На этот вопрос можно ответить измерениями. Причём довольно быстро, если знать, какие четыре числа и в каком порядке смотреть и какая контрольная проверка превращает подозрение в диагноз.
Эта статья посвящена исключительно диагностике. Речь не о том, как отбить атаку, а о том, как безошибочно отличить её от проблемы с нагрузкой, от программной ошибки или просто от внезапного успеха проекта. Тем, кто хочет понять, что такое DDoS-атака с технической стороны, стоит начать со статьи Что такое DDoS-атака. Тем, кто уже получил диагноз и собирается действовать, пригодится Защита сервера от DDoS-атак.
Четыре подозреваемых, один симптом
«Сервер тормозит» — это не диагноз, а симптом как минимум с четырьмя правдоподобными причинами. Прежде чем набрать первую команду, стоит понимать, какие именно картины вы собираетесь различать.
| Наблюдение | Атака | Настоящий наплыв посетителей | Программная ошибка |
|---|---|---|---|
| Начало | резко, за считанные секунды | рост в течение минут, обычно плавной кривой | сразу после развёртывания, cron-задания или обновления |
| Входящая сетевая нагрузка | высокая или экстремальная, часто множество мелких пакетов | умеренная, исходящая заметно выше входящей | без особенностей |
| Соединений на один IP-адрес источника | очень много с немногих адресов либо очень мало с очень многих | распределено равномерно, по нескольку на адрес | в норме |
| Referrer в логе | чаще всего пустой | новостные сайты, соцсети, поисковые системы | в норме |
| Ответ через 127.0.0.1 | быстрый (проблема в сети) | медленный (проблема в приложении) | медленный или с ошибкой |
| После перезапуска службы | нагрузка возвращается сразу | нагрузка возвращается, картина ошибок та же | проблема часто пропадает на несколько минут |
Четвёртого подозреваемого в этой таблице нет, потому что отдельная колонка ему не нужна: это плановые работы. Резервное копирование, переиндексация, ротация логов и обновление пакетов запускаются в предсказуемое время. Взгляд на systemctl list-timers и на crontab занимает десять секунд и закрывает удивительно много подозрений на атаку.
Первые 60 секунд: четыре числа
Снимите четыре значения именно в этом порядке. Показательна их комбинация, каждое значение по отдельности не говорит почти ничего.
cat /proc/loadavg
ss -s
cat /proc/net/dev
curl -o /dev/null -s -w '%{time_total}\n' http://127.0.0.1/
Как это читать:
- Высокая нагрузка, высокая сетевая нагрузка, много полуоткрытых соединений, но 127.0.0.1 отвечает за миллисекунды: проблема находится перед приложением, то есть на сетевом уровне. Это классическая картина атаки.
- Высокая нагрузка, обычная сетевая нагрузка, 127.0.0.1 отвечает медленно: дело в приложении или базе данных. Это не атака, а работа для разработчика.
- Низкая нагрузка, высокая сетевая нагрузка: очень подозрительно. Поток пакетов, который вообще не доходит до приложения, тратит мало CPU и много канала.
- Всё низкое, но снаружи служба всё равно недоступна: смотрите ниже, раздел про случаи без измеримых следов.
Про само значение нагрузки: /proc/loadavg считает и процессы, ожидающие ввода-вывода. Значение 40 на четырёх ядрах может означать и атаку, и перегруженный накопитель. vmstat 1 5 разделяет эти случаи чётко: колонка r показывает процессы, готовые к выполнению, b заблокированные, а wa долю времени ожидания.
Считаем соединения: ss вместо netstat
Почти все инструкции в интернете начинаются с netstat. На актуальной системе это заканчивается так:
Command 'netstat' not found, but can be installed with:
apt install net-tools
netstat входит в пакет net-tools, которого в стандартной установке Debian 12, Debian 13, Ubuntu 22.04 и Ubuntu 24.04 больше нет. В семействе Red Hat (AlmaLinux, Rocky, RHEL, Oracle Linux) его тоже нет. Доустановить пакет можно, но разумнее взять ss из iproute2: он есть практически на любом сервере, заметно быстрее при большом числе соединений и выдаёт ровно ту же информацию.
ss -s
Чтобы все дальнейшие команды отработали, понадобится небольшой набор пакетов, и называются они в разных семействах дистрибутивов по-разному. Именно здесь скопированная инструкция чаще всего застревает на AlmaLinux уже в первой строке:
| Инструмент | Debian и Ubuntu | AlmaLinux, Rocky, Oracle Linux |
|---|---|---|
| ss, nstat, ip | iproute2 | iproute |
| netstat | net-tools | net-tools |
| vmstat, free, top | procps | procps-ng |
| sar | sysstat | sysstat |
| dig | dnsutils | bind-utils |
| tcpdump | tcpdump | tcpdump |
То есть на Debian и Ubuntu apt-get install -y iproute2 net-tools procps sysstat dnsutils tcpdump, а в семействе Red Hat dnf -y install iproute net-tools procps-ng sysstat bind-utils tcpdump. curl в этот список лучше не добавлять: он уже установлен как curl-minimal, и полный пакет с ним конфликтует (curl-minimal ... conflicts with curl). Если он всё же нужен, поможет dnf -y --allowerasing install curl.
Первая строка выводит общее число сокетов, строка TCP разбивает их по состояниям: estab, closed, orphaned, timewait. Само по себе высокое значение timewait не признак атаки, а нормальное следствие множества коротких HTTP-соединений.
Интереснее распределение по состояниям:
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn
Настораживает крупный блок в состоянии SYN-RECV. Эти соединения были начаты, но так и не подтверждены, а это и есть картина SYN-флуда. Посчитать их можно напрямую:
ss -Htn state syn-recv | wc -l
Ловушка со столбцами, на которой ломается большинство однострочников
Как только вы передаёте ss фильтр по состоянию, колонка состояния из вывода исчезает. Удалённая сторона оказывается в четвёртом столбце вместо пятого. Именно поэтому скопированные с форумов однострочники регулярно выдают бессмыслицу: они считают порты вместо IP-адресов или печатают пустые строки. Правило простое: без фильтра удалённая сторона находится в $5, с фильтром в $4. Ключ -H дополнительно убирает строку заголовка, поэтому wc -l считает верно без поправок.
Соединения по IP-адресам источника, корректно и для IPv6:
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -20
Команда sed отрезает только последнее двоеточие вместе с портом. Распространённый вариант с cut -d: -f1 работает для IPv4, но режет IPv6-адрес после первого блока и обесценивает весь подсчёт. У IPv6 остаются квадратные скобки, подсчёту это не мешает.
Интерпретировать результат нужно с чувством меры. Сто соединений с одного адреса могут быть атакой, а могут быть корпоративным NAT, NAT мобильного оператора или reverse proxy перед вашим сервером. Если впереди стоит сеть доставки контента или балансировщик, вы всё равно увидите только его адреса и вам придётся смотреть заголовок X-Forwarded-For в логе веб-сервера.
Для UDP та же схема с ключом -u. А чтобы вообще понять, какая служба на каком порту слушает:
ss -tulnp
Сетевая нагрузка и интенсивность пакетов: как измерять правильно
Полосу в мегабитах называют все. Куда показательнее интенсивность пакетов. Атака в 200 000 крошечных пакетов в секунду кладёт сервер, хотя полоса при этом выглядит совершенно безобидно.
Сначала имя интерфейса, потому что eth0 давно не везде правильный ответ. Обычные варианты: ens3, enp1s0 или eth0:
ip -br link
Дальше два замера с интервалом в секунду, вообще без дополнительных пакетов. Имя интерфейса при этом берётся из маршрута по умолчанию, а не вписывается вручную:
IF=$(ip -o route get 1.1.1.1 | awk '{print $5}')
A=$(cat /sys/class/net/$IF/statistics/rx_packets) || exit 1; sleep 1; B=$(cat /sys/class/net/$IF/statistics/rx_packets); echo "$((B-A)) входящих пакетов/с на $IF"
У этой громоздкости есть весомая причина. Если вписать сюда жёстко eth0, а интерфейс на самом деле называется ens3 или enp1s0, как это бывает на большинстве KVM-серверов, команда дважды выдаст cat: /sys/class/net/eth0/statistics/rx_packets: No such file or directory, после чего невозмутимо сообщит 0 входящих пакетов/с и завершится с кодом возврата 0. В статье про обнаружение атак это самый опасный из всех вариантов: вы читаете ноль пакетов, даёте отбой, а атака продолжается. Именно это и предотвращает || exit 1.
То же самое с rx_bytes даёт байты в секунду. Из двух значений вы считаете средний размер пакета, а он выдаёт тип атаки:
- меньше 100 байт в среднем при очень высокой интенсивности пакетов: SYN-, ACK- или UDP-флуд. Цель — обработка пакетов, а не канал.
- от 1200 до 1500 байт при высокой полосе, порты источника 53, 123, 389 или 11211: атака с отражением и усилением. IP-адреса источника принадлежат чужим серверам, а не атакующим.
- обычное распределение размеров, корректные HTTP-запросы: уровень приложения. Тогда решает лог веб-сервера, а не счётчик пакетов.
Удобнее это делать через sar из пакета sysstat:
sar -n DEV 1 3
Подводный камень: измерение в реальном времени работает сразу после установки, а вот историческая выборка через sar -f нет. На Debian и Ubuntu сбор данных выключен по умолчанию, в /etc/default/sysstat стоит ENABLED="false". Кто замечает это только во время атаки, остаётся без данных для сравнения за позавчера. Именно поэтому пакет стоит включить заранее, а не в момент инцидента.
И самое важное ограничение из всех: внутри сервера вы измеряете только то, что до него дошло. Если перед ним стоит фильтрация, вы увидите долю реального объёма или вообще ничего. Достоверное число находится в графике трафика в личном кабинете, а не в /proc/net/dev.
Читаем сообщения ядра
Ядро протоколирует ситуации перегрузки, которые на уровне приложения остаются невидимыми. На Ubuntu и в актуальных версиях Debian кольцевой буфер закрыт для обычных пользователей, без sudo появляется:
dmesg: read kernel buffer failed: Operation not permitted
Это не сбой, а kernel.dmesg_restrict=1. С правами root и с отметками времени:
dmesg -T | grep -Ei 'syn flood|conntrack|neighbour|drop'
Сообщение, которое ищут чаще всего, выглядит дословно так:
TCP: request_sock_TCP: Possible SYN flooding on port 443. Sending cookies. Check SNMP counters.
Есть два варианта этого сообщения и одно различие в формате, о котором стоит знать:
- Sending cookies означает, что SYN-cookies активны и соединения продолжают обслуживаться. Dropping request появляется вместо этого, когда
net.ipv4.tcp_syncookiesравен 0. Тогда запросы отбрасываются, вместе с настоящими посетителями. - Старые ядра называют только номер порта, новые дополнительно адрес прослушивания в виде
0.0.0.0:443. Поэтому на Debian 12 вы увидите короткую форму, а на Debian 13 и Ubuntu 24.04 длинную. Кто ищет grep-ом точную старую формулировку, на новых системах не найдёт ничего.
Важно для честности диагноза: это сообщение не доказывает атаку. Оно появляется и тогда, когда приложение работает со слишком маленьким listen backlog, а его накрывает легитимный всплеск нагрузки. Это признак, который должен подтверждаться другими измерениями.
Соответствующие счётчики выдаёт nstat:
nstat -az TcpExtSyncookiesSent
nstat -az TcpExtListenDrops
nstat -az TcpExtListenOverflows
И здесь есть подводный камень: nstat запоминает при вызове промежуточное состояние и в следующий раз показывает только разницу. Это сделано намеренно и для измерений даже удобно, но удивляет каждого, кто при втором вызове внезапно видит нули. Ключ -a заставляет выводить абсолютные значения, ключ -s отключает запись состояния.
Ещё два сообщения, которые регулярно всплывают во время атаки и оба ведут к потере пакетов у легитимных посетителей:
nf_conntrack: table full, dropping packet
neighbour: arp_cache: neighbor table overflow!
Заполненность таблицы отслеживания соединений проверяется через cat /proc/sys/net/netfilter/nf_conntrack_count в сравнении с nf_conntrack_max. Оба файла существуют, только если модуль загружен, то есть если активен файрвол. На системе без правил их нет, и это нормально.
Логи веб-сервера: закономерность вместо чутья
На Debian и Ubuntu логи лежат в /var/log/nginx/access.log или соответственно в /var/log/apache2/access.log. В семействе Red Hat путь к логу Apache называется /var/log/httpd/access_log, без точки перед окончанием. Для начала хватит трёх выборок:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
Отличить атаку от настоящего наплыва посетителей позволяет соотношение этих трёх списков:
- Соотношение запросов и уникальных IP-адресов. Десять тысяч запросов с 6000 адресов — это аудитория. Десять тысяч запросов с 30 адресов уже нет.
- Дочерние запросы. Настоящий посетитель после HTML-страницы подгружает таблицы стилей, скрипты и картинки. Если в списке путей стоит одна-единственная ссылка и ни одного статического файла, браузера там не было.
- Источник перехода. При удачной рекламной кампании в поле Referrer стоит источник: новостной сайт, соцсеть, поисковая система. При атаке это поле обычно пустое или заполнено выдуманным адресом.
- Идентификатор программы. Один и тот же, посимвольно совпадающий User-Agent на десятках тысяч запросов — это инструмент. Настораживают и идентификаторы очень старых версий браузеров.
- Коды ответа. Преобладание 200 говорит об аудитории, стена из 404 или 499 о том, что кто-то автоматически перебирает адреса, либо о клиентах, которые обрывают соединение до ответа.
Осторожно с поисковыми системами: адрес охотно выдаёт себя в User-Agent за Googlebot. Проверить это можно только обратным, а затем прямым разрешением имени. Утверждение верно лишь тогда, когда обратное имя указывает на домен поисковика и это же имя снова разрешается в тот же IP-адрес.
dig -x 66.249.66.1 +short
Для настоящего адреса Googlebot возвращается имя вида crawl-66-249-66-1.googlebot.com., которое вы затем командой dig +short crawl-66-249-66-1.googlebot.com разрешаете обратно в тот же IP-адрес. Если не возвращается вообще ничего, причиной может быть и сервер без исходящего разрешения DNS, и тогда это не доказательство.
И самое важное предложение про логи вообще: атака на сетевом уровне в логе веб-сервера не отражается. SYN-флуд никогда не доходит до приложения и не оставляет там ни одной строки. Пустой лог, таким образом, не опровергает ничего, он лишь сужает уровень.
Контрольная проверка: как подозрение превращается в диагноз
До сих пор у вас были только признаки. Убедительными их делают контрольные проверки, каждая из которых способна опровергнуть ровно одну гипотезу.
- Остановить службу. Остановите веб-сервер на 30 секунд. Если входящая интенсивность пакетов остаётся такой же высокой, атака идёт ниже уровня приложения. Если она обваливается, это были запросы к вашему приложению, злонамеренные или нет.
- Изнутри против снаружи. Если
curlчерез 127.0.0.1 отвечает за миллисекунды, а запрос снаружи упирается в тайм-аут, приложение здорово, а проблема в канале. - Второй замер. Повторите измерение пакетов через две минуты. Атаки продолжаются или возвращаются волнами. Единичный всплеск был всего лишь всплеском.
- Взгляд снаружи. Внешняя проверка доступности из нескольких точек отделяет «недоступно для всех» от «недоступно только для вас». Второй случай обычно означает проблему с маршрутизацией или с провайдером у самого проверяющего, а не атаку.
- Проверить направление. Сравните
rx_packetsсtx_packets. Если необычно выглядит исходящая интенсивность, атакуют не вас, а ваш сервер атакует сам. Значит, он скомпрометирован или используется как отражатель, и срочность случая мгновенно меняется.
Признак того, что диагноз состоятелен: вы можете одним предложением сказать, какой протокол на какой порт с какой интенсивностью приходит, входящий он или исходящий, и у вас есть два независимых измерения, которые показывают одно и то же. Всё, что ниже этой планки, остаётся догадкой.
Когда измеримого следа нет
Четыре ситуации, которые регулярно вызывают путаницу:
- Вы вообще не попадаете на сервер. При забитом канале даже SSH не пробивается. Доступ тогда идёт через консоль в личном кабинете, которая работает независимо от сетевого подключения системы. Ровно для этого она и нужна.
- Служба пропадала, а в сервере ничего нет. При фильтрации перед сервером это правило, а не исключение. Если атака перехвачена в сети, система видит лишь короткую просадку. Доказательство в этом случае находится в графике трафика в личном кабинете.
- Лог обрывается посреди инцидента. Проверьте, не прошла ли тем временем ротация логов: более старый материал лежит рядом как
access.log.1илиaccess.log.2.gz. Еслиaccess_logв конфигурации отключён или буферизуется, последних строк просто нет. - Ядро молчит. На контейнерных системах кольцевой буфер принадлежит хост-системе,
dmesgтам не показывает ничего своего. На KVM root-сервере с собственным ядром такой проблемы нет.
Что задокументировать, прежде чем писать провайдеру
Обращение в духе «сервер сегодня днём тормозил» удлиняет обработку на несколько кругов. Обращение с результатами измерений обрабатывается сразу. Собирайте данные, пока инцидент идёт, потому что счётчики в /proc сбрасываются при перезагрузке.
mkdir -p /root/incident && cd /root/incident
date -u > 01-time.txt
ss -s > 02-sockets.txt
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn > 03-states.txt
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -50 > 04-top-ips.txt
ip -s link > 05-interfaces.txt
sar -n DEV 1 10 > 06-packet-rate.txt
dmesg -T | tail -100 > 07-kernel.txt
nstat -az > 08-counters.txt
Если канал позволяет, к этому добавляется короткий захват пакетов. Ограничьте его: неограниченный захват на забитом канале заполняет накопитель за минуты и усугубляет проблему:
tcpdump -D
tcpdump -ni "$IF" -s 96 -c 2000 -w /root/incident/capture.pcap
Команда tcpdump -D перечисляет доступные интерфейсы на случай, если переменная $IF из раздела с измерениями выше больше не задана. Оба вызова требуют root-прав.
В тикет тогда входят:
- Время начала и окончания с указанием часового пояса.
date -uснимает любые споры на эту тему. - Затронутый IP-адрес и затронутый порт.
- Измеренная интенсивность пакетов и полоса, обязательно с указанием направления.
- Распределение по протоколам и, если оно различимо, порты источника удалённых сторон.
- Несколько примеров адресов источника с оговоркой, что отправители могут быть подделаны.
- Выдержка из лога веб-сервера с повторяющимся шаблоном запроса, трёх-пяти строк достаточно.
- Что менялось незадолго до этого: развёртывание, смена DNS, рекламная кампания, новое открытие порта.
- Результат контрольной проверки: была ли служба доступна через 127.0.0.1, пока снаружи она не отвечала?
Частые ошибочные выводы
- Много соединений в TIME-WAIT как доказательство атаки. Это нормальное следствие коротких HTTP-соединений, и оно проходит само.
- Сообщение о SYN flooding как доказательство. Слишком маленький listen backlog порождает его и при легитимном всплеске нагрузки.
- Блокировать адрес с большим числом соединений, не проверив его. За корпоративным NAT, за NAT мобильного оператора или за прокси вы так закрываете доступ целым группам клиентов.
- Делать вывод об атаке из высокой полосы. Сначала проверьте направление. Исходящие всплески — это чаще всего резервное копирование или популярный файл на скачивание.
- Делать вывод «атаки нет» из пустых логов. Атаки на сетевом уровне до приложения не доходят.
- Перезагружать во время идущего инцидента. Перезагрузка стирает все счётчики, которые понадобились бы для обращения, а атака после неё продолжается как ни в чём не бывало.
Кто один раз отработал эту последовательность, получает состоятельный диагноз меньше чем за пять минут. А кто прикладывает измерения к тикету, пропускает круг уточняющих вопросов и сразу переходит к решению. По теме: Чек-лист для нового root-сервера и Настройка Fail2ban для доработки на уровне приложения.
Частые вопросы
Как отличить DDoS-атаку от настоящего наплыва посетителей?
Почему netstat не установлен на моём сервере?
Всегда ли сообщение Possible SYN flooding on port 443 означает атаку?
Почему на сервере нет никаких следов, хотя служба ненадолго была недоступна?
Как попасть на сервер, если SSH во время инцидента больше не отвечает?
Почему скопированный однострочник с ss выдаёт неверные числа?
Что делать, если необычно выглядит исходящая интенсивность пакетов?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

