Есть у меня давний клиент, называть не буду (лишний нецелевой трафик ему сейчас совсем ни к чему), человека три-четыре с форума точно знают, о ком речь

Один в поле не воин
Первое, сразу же: даже не думайте, что справитесь в такой ситуации своими силами, правильно настроив сервер и набив огромный список IP в конфиг файрвола. Даже если у вас физический выделенный сервер, его вычислительные ресурсы и пропускная способность канала - величины конечные. Sapienti sat.
Опыт, если что, у меня уже был

За широкой спиной
Сейчас каждый приличный хостер предлагает защиту. В данном случае хостера зовут FirstVDS, и предлагает он спрятаться за широкую спину своего партнёра DDOS Guard. Не сочтите за рекламу, да реклама тут и бесполезна: у каждого хостера свой партнёр по этой части, именно за его спиной вы при поддержке хостера и окажетесь.

Небольшая иллюстрация. Видим, собственно, момент начала атаки и вполне закономерный результат. Синим цветом показан входящий трафик, красным исходящий (всё в GiB).
В самом начале VDS просто "погас", все его попытки отдавать трафик выглядели просто смехотворно (первый пик). Далее проблема была замечена, и я решил побороться за живучесть (масштаб не смутил, ага)

После этого синее и красное почти сравнялись и что-то как-то заработало, харкая кровью, но следующий пик вновь отправил VDSку в аут. Вместо страниц пошли лаконичные системные сообщения, что SQL сервис куда-то унесён ветром. Поминутно строчить "systemctl restart mariadb.service" в еле дышащей консоли - так себе развлечение, да и техподдержка начала ненавязчиво подсказывать "что-то с вами неладно, похоже, ддосят вас". Поговорил с клиентом (платить-то ему), и мы быстро оказались за крепостной стеной DDOS Guard.
Рано радуетесь или Проблемы мелкого шрифта
Нет, вообще-то хостеры не виноваты, что мы не всё читаем заранее. И что мы не сразу оцениваем прочитанное - тоже не на их совести. Просто заметьте себе: при переходе на защищённый IP вы заодно оказываетесь на канале, который стоит дороже аренды сервера с парой Xeon'ов на борту (это если вы на colocation). Или не дороже самого начального VDS, но ширина канала порядка на три поменьше. И трафик сверх нормы сразу начинает иметь значение. В смысле, денежное. Которое мы заранее никогда не можем просчитать и прикинуть.
Да, если вы этого клиента привели по своей партнёрке, можете потирать руки, к вам потечёт приятный ручеёк от его переплат. Но ручаюсь, ему не очень понравится платить по 12-16 тыр в месяц там, где он привык платить не больше одной. Поэтому некоторые меры лучше принять и оказавшись "за широкой спиной". И самое первое - проверьте, не выложены ли на сайте какие-нибудь тяжёлые файлы для скачивания. Я вот поздно спохватился - на сайтах было кое-что выложено, и немалых размеров, но поначалу практически весь напор атакующих дебилов рвался на морду. Как видно на картинке, ситуация сперва изменилась к лучшему, а потом кто-то (не одни же дебилы атакуют!) просканировал сайты чем-то вроде Xenu и пошли обращения по всем найденным ссылкам, не только на html-страницы.
Когда презентацию в 30 с лишним мег начинают качать массово, это очень впечатляет. Файлы, конечно, не скачивали полностью (по логам я видел, что это скучное занятие обрывалось самое позднее на половине), но когда ddos'ят по всему набору найденных ссылок, это скачивание идёт не один раз в минуту и исходящий трафик растёт очень шустро.
Так что сразу уберите ссылки "скачать" до лучших времён. Или хорошенько поиграйтесь с JS, чтобы эти ссылки были видны только людям. Ноябрьский подъём на графике наглядно показывает разницу.
Защита от dDoS на php
Смешно? Да, согласен, очень смешно, когда флуд пытаются отсечь такими средствами на уровне движка. Но в данном случае я веду речь не о попытке остановить лавину совочком, а лишь о том, что никакой сервис защиты от dDoS не даст вам полной фильтрации трафика. Откровенный мусор, конечно, отправят в /dev/nul на дальних подступах, но немало "правдоподобного" HTTP-флуда дойдёт и до сервера, заставляя его впустую генерировать страницы и исходящий трафик.
Скажем так, до DDOS Guard'а был у сайта access_log HTTP чуть не в полгига, а стал в 150 мег, хотя в лучшие времена больше пяти мегабайт не бывало. Сайты у клиента - скромные визитки со скромным трафиком... были, до середины сентября прошлого года. А если бы не так скромно, что тогда? Так что мой Внутренний Перфекционист (ВП) задумался, что ещё с этим можно поделать.
Смотрим в логи - видим большую стаю дятлов, более-менее успешно притворяющихся браузерами. Характерные признаки понятны: кидают запросы раз за разом с максимально возможной частотой с одних и тех же IP, получают только html-код, из которого никаких прилинкованных объектов не запрашивают. Ни картинок, ни CSS, ни JS. Только то, что по ссылкам с a href=...
Из манеры поведения понятен и алгоритм их определения.
Берём, к примеру, запросы за интервал в 5 секунд. По скольким ссылкам нормальный посетитель за это время перейдёт? Вряд ли больше чем по двум - зашёл на одну страницу, быстро глянул, кликнул по ссылке и ушёл на другую. Значит, считаем, что если за 5 секунд кто-то отправил больше 2 запросов - это явно не человек. Для спокойствия берём норму в 3 запроса, после третьего отправляем IP в стоп-лист минут на 10.
И на любой запрос с IP-адресов из стоп-листа отвечаем отправкой статус-кода "304 Not Modified". Это мой ВП так решил.
Логично же - в ответ на повторный запрос любезно сообщить боту, что за прошедшие доли секунды запрошенный документ не изменялся. А отправлять тело документа в этом случае не позволяет Его Величество Протокол HTTP. Засим всего Вам хорошего, Ваше Неправдоподобие, будете проходить мимо - проходите. С ботами я всегда общаюсь строго в рамках протокола, как предписывают нормы дипломатии.
Ведём учёт запросов за 5 секунд в файле. Формат данных по вашему вкусу, а мне по вкусу формат CSV, в нём и сохранял. Данные:
- IP-адрес;
- время первого запроса с IP (проще всего таймштамп, выхваченный функцией time() - удобно секунды отсчитывать)
- порядковый номер запроса с этого IP за 5 секунд
Как только проходит третий запрос за интервал, IP-адрес заносится в файл стоп-листа вместе со временем занесения. Из списка "за 5 сек" его нужно убрать, он теперь штрафник.
Итого: делаем маленький скриптик, который:
- проверяет, есть ли этот IP в стоп-листе и не истекло ли время (10 минут отсидки). Если есть и время не вышло, сразу же посылает статус 304 и exit() - чтобы не тратить ресурсы зря.
- проверяет, есть ли этот IP в "5-секундном" списке. Если нет, то заносит. Если есть, то увеличивает счётчик запросов.
- Естественно, при каждом обращении к файлам списков нужно удалять из них устаревшие данные (для этого и нужны таймштампы занесения в список).
У любого движка сегодня есть "центральный" скрипт, на который и попадают все запросы. Вот в него и вписываем на самый верх include нашего скрипта-учётчика.
Лирическое отступление. Внешние Перфекционисты (а они здесь есть, я знаю) уже наверняка готовы к критике. Я о хранении данных в файлах, разумеется. Да, при такой плотности запросов коллизии случаются неизбежно, это я увидел в логе ошибок по сообщениям о пустых массивах данных. Но к моему удивлению коллизий было совсем немного, после них работа дятлорезки очень быстро восстанавливалась и в логах выстраивались стройные колонны статусов 304

Однако, чтобы довести этот инструмент до ума, лучше использовать блоки shared memory вместо файлов, с ними и быстрее, и с блокировкой при записи проще. Так что сейчас переделываю с применением группы функций shmop_*().
Ещё немного добавим...
В качестве дополнительного критерия можно проверить, запрашивались ли за последние 10 минут с данного IP, к примеру, файлы CSS. Браузер-то их точно запросит, так что если "дятлом" оказался некий легальный прокси и CSS запрашивались, это позволит не банить его зря.
Делается это несложно. Предположим, внутри DOCUMENT_ROOT сайта есть директория /css и в ней лежит файл default.css, который задействован на каждой странице. И если мы его будем грузить не напрямую, а через php-скрипт, это нам позволит заодно и зафиксировать адрес, с которого запрашивали. Будем организовывать учёт запросов в самодельном лог-файле. Формат данных - CSV, разделитель - пробел, каждый адрес в своей строке, структура строки вот такая:
IP-address timestamp count
Назовём этот скрипт csslog.php и положим его прямо в директорию /css, код скрипта выглядит вот так:
<? // куда складываем лог запросов CSS? $logfile = $_SERVER['DOCUMENT_ROOT'].'/botlog/css.log'; // новое имя для default.css $cssfile = "_default.css"; // фиксируем IP-адрес $ip = $_SERVER['REMOTE_ADDR']; // фиксируем время в виде таймштампа $tst = time(); if (file_exists($logfile)) $ar = file($logfile); else $ar = array(); $lar = array(); foreach ($ar as $val) { $row = explode(' ',trim($val)); if ($tst - intval($row[1]) < 600) { $lar[$row[0]] = $row; if ($row[0] == $ip) ++$lar[$row[0]][2]; } } if (!isset($lar[$ip])) { $lar[$ip] = array( $ip, $tst, 1 ); } $str = ''; foreach ($lar as $val) { $str .= implode(' ',$val)."\n"; } $fh = fopen($logfile, 'w'); fwrite($fh, $str); fclose($fh); header('Content-type: text/css'); readfile($cssfile); ?>Что он должен делать? Он должен реагировать на запросы файла default.css - сперва засечь время запроса и IP-адрес, с которого запросили, потом считать и разобрать наш лог-файл, где мы такие запросы фиксируем (для этих логов делаем в корне сайта директорию /botlog), и пометить там, в который раз за последние 600 секунд с этого IP запрашивали CSS.
Заодно скрипт удаляет записи, которые старее 600 секунд и перезаписывает лог. После чего просто отправляет соответствующий HTTP-заголовок и вслед за ним тело CSS-файла.
Файл default.css переименовываем (в примере на _default.css), чтобы он не был доступен напрямую по имени. И организовываем перехват запросов к этому файлу, подсовывая вместо него наш скрипт.
Для этого в директории /css делаем файлик .htaccess с такой вот начинкой:
RewriteEngine on RewriteBase /css/ RewriteRule ^default\.css$ csslog.php [L]Этот пример - не догма, а руководство к действию. Можно таким способом проверять не CSS-файл, а файл какой-нибудь картинки или Java-скрипта - нужно, чтобы этот файл использовался на всех страницах, тогда проверка IP-адреса на "человечность" будет надёжной.
Итог
В конечном счёте удалось довести