Pull to refresh

Простой способ защиты от классического HTTP DDoS

Reading time5 min
Views35K
Данное решение позволяет вычислять любых ботов, за исключением тех, которые полностью имитируют работу браузера.

Как это работает


Бот запрашивает страницу, например habrahabr.ru/search. Бот не умеет загружать вместе со страницей картинки, скрипты, css и пр. Значит в логе будет отображен запрос к /search/ и всё.
Если на habrahabr.ru/search заходит живой человек через браузер, то вместе с /search/ в лог попадет множество картинок, скриптов, css и пр.

Настройка


Mysql


/etc/my.cnf
[mysqld]
local-infile=1 #разрешаем load data
# устанавливаем максимальный размер мемори:
max_heap_table_size=1024M
tmp_table_size=1024M

Под рутом:
UPDATE `mysql`.`user` SET `File_priv` = 'Y' WHERE `user`.`Host` = 'localhost' AND `user`.`User` = 'ИМЯ_ЮЗЕРА_БД'; flush privileges;

sysctl


sysctl.conf подробно с комментариями (linux)

Ram drive


Ram drive нужен для ускорения работы с логами nginx-а.
Добавляем в файл /etc/fstab
tmpfs /var/log/ram_disk tmpfs size=1024m 0 0

Затем
mkdir /var/log/ram_disk
mount -t tmpfs -o size=1024m tmpfs /var/log/ram_disk


Алгоритм



1. Выбор ловушки


Берем на сайте любой статичный и ничем не приметный файл (картинка, css, js и пр.), загружаемый при вызове любой страницы динамики, например habrahabr.ru/styles/fontello/css/habr.css
Этот файл нужно сделать некэшируемым, т.е. добавить рандомный параметр, например <?php echo '/styles/fontello/css/habr.css?'.rand(99999999)?>.
Для справки, по умолчанию opera кладет в локальный кэш картинки на 1 час, css/js на 5 минут.

2. Правим конфиг nginx


# делаем нужный нам формат лога
log_format ddos_log '$remote_addr\t$msec\t$status';

# наша ловушка
location =/styles/1347283218/highlight.css {
	access_log  /var/log/ram_disk/hook_access.log ddos_log;
}

# вся остальная статика
location ~* ^.+\.(class|htc|bmp|cur|jpg|jpeg|gif|png|svg|xls|doc|xhtml|js|css|mp3|ogg|mpe?g|avi|flv|zip|gz|bz2?|rar|ico|txt|jar|swf)$ {
	access_log  off;
}

# динамика
location / {
	access_log  /var/log/ram_disk/dynamic_access.log ddos_log;
}



3. Создаем таблицы для логов


ENGINE=MEMORY — чтобы было быстрее.
CREATE TABLE `dinamic_log` (
  `inc` bigint(20) NOT NULL AUTO_INCREMENT,
  `remote_addr` varchar(20) NOT NULL DEFAULT '0',
  `time_local` int(20) NOT NULL DEFAULT '0',
  `status` int(4) NOT NULL DEFAULT '0',
  PRIMARY KEY (`inc`),
  KEY `remote_addr` (`remote_addr`),
  KEY `time_local` (`time_local`)
) ENGINE=MEMORY AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

CREATE TABLE `hook_log` (
  `inc` bigint(20) NOT NULL AUTO_INCREMENT,
  `remote_addr` varchar(20) NOT NULL DEFAULT '0',
  `time_local` int(20) NOT NULL DEFAULT '0',
  `status` int(4) NOT NULL DEFAULT '0',
  PRIMARY KEY (`inc`),
  KEY `remote_addr` (`remote_addr`),
  KEY `time_local` (`time_local`)
) ENGINE=MEMORY AUTO_INCREMENT=1 DEFAULT CHARSET=latin1


Таблица, куда занесем ip поисковых ботов
CREATE TABLE `white` (
  `remote_addr` bigint(20) NOT NULL,
  PRIMARY KEY (`remote_addr`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1

Таблица заблокированных
CREATE TABLE `black` (
  `remote_addr` bigint(20) NOT NULL,
  `time_local` int(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`remote_addr`),
  KEY `time_local` (`time_local`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1


4. Главный скрипт


Для простоты понимания написано на php, т.к. этот язык знают почти все. И обработка ошибок убрана также — для простоты понимания.

// файл лога динамики
$dinamic_log = $argv[1];
// файл лога ловушки
$hook_log = $argv[2];
// кол-во запросов к динамике без картинок, после которого будем банить.
$r_stop = $argv[3];
// период за который делаем анализ (храним данные в таблице)
$load_time = $argv[4];
// через сколько сек делаем загрузку лога и анализ
$wait_sec = $argv[5];

function load_log($log, $table) {
	$tmp = '/var/log/ram_disk/tmp_ddos_file';
	// копируем лог во временный файл
	copy ($log, $tmp);
	// чистим лог
	file_put_contents($log, "", LOCK_EX);
	// загружам лог в БД
	mysql_query('LOAD DATA CONCURRENT INFILE "'.$tmp.'" IGNORE INTO TABLE '.$table.' FIELDS TERMINATED BY \'\t\' (`remote_addr`, `time_local`, `status`) SET `remote_addr` = INET_ATON(`remote_addr`)');
	// удалем временный файл
	unlink($tmp);
}

// выполняем в бесконечном цикле
while (true) {
	// загружаем лог динамики
	load_log($dinamic_log, 'dinamic_log');
	
	// загружаем лог ловушки
	load_log($hook_log, 'hook_log');

	// ищем ботов. nginx не дает проверять $status, поэтому 200 и 304 фильтруем здесь.
	$res = mysql_query('SELECT dinamic_log.remote_addr FROM `dinamic_log` WHERE (`status` = 200 OR `status` = 304) AND`remote_addr` NOT IN (SELECT `remote_addr` FROM `hook_log`) AND`remote_addr` NOT IN (SELECT `remote_addr` FROM `white`) GROUP BY `remote_addr` HAVING count(inc)>'.$r_stop);
	while ($row = mysql_fetch_array($res))
	{
		// логируем забаненые ip
		mysql_query('INSERT INTO black(`remote_addr`) VALUES ('.$row['remote_addr'].')');
		// блокируем ip
		switch (PHP_OS) {
			case "FreeBSD":
				system('/sbin/route add -host '.$row['remote_addr'].' 127.0.0.1 -blackhole');
				break;
			case "Linux":
				system('/sbin/ip route add blackhole '.long2ip($row['remote_addr']));
				break;
		}			
	}
	// чистим таблицу от старых логов
	mysql_query('DELETE FROM `log` WHERE `time_local` < (UNIX_TIMESTAMP() - '.$load_time.')');
	// ждем
	sleep($wait_sec);
}

Запускаем:
php ddoshook.php /var/log/ram_disk/dynamic_access.log /var/log/ram_disk/hook_access.log 5 300 3


5. Разбаниваем


$block_time = $argv[1]; // на какое время баним ip.
$res = mysql_query('SELECT `remote_addr` FROM black WHERE time_local < (UNIX_TIMESTAMP() - '.$block_time.')');
while ($row = mysql_fetch_array($res)) {
	// разблокировка ip
	switch (PHP_OS) {
		case "FreeBSD":
			system('/sbin/route delete '.$row['remote_addr']);
			break;
		case "Linux":
			system('/sbin/ip route delete '.long2ip($row['remote_addr']));
			break;
	}	
}

Помещаем в крон
* * * * * /usr/bin/php unban.php 86400


Вот и всё, боты банятся, люди пропускаются.

В следующих выпусках:
  • Как выдерживать syn/udp/icmp flood на пределах возможностей сервера и канала.
  • Как безошибочно определять поисковых ботов, не обращая внимания на юзер-агенты.
  • 7 методов, которые помогли отбить более 1000 http ddos-атак.
  • Как получить профессиональную защиту от ddos-атак, заплатив всего 5$.


upd

Как узнать IP поисковых ботов?


  1. Ищем AS нужного поисковика, например здесь: bgp.potaroo.net/cidr/autnums.html
  2. Получаем IP адреса для AS: stat.ripe.net/data/announced-prefixes/data.json?resource=AS15169

Cписки AS и IP нужно постоянно обновлять.

upd 2

OS: FreeBSD 8.3
CPU: E5-2620 2.00GHz

Тест 1


rows(dinamic_log): 100000 (100 000 http-запросов к динамике сайта за 3 секунды)
rows(hook_log): 1000 (1000 легитимных запросов от пользователей за 3 секунды)

# php /root/scripts/php/imgtest/ddos_hook.php /tmp/d20.log /tmp/h20.log 5 300 3
LOAD DATA time elapsed: 0.29 sec.
LOAD DATA time elapsed: 0.003 sec.
select time elapsed: 0.017 sec.
rows(ban): 1800
full cicle time elapsed: 0.313 sec.

Тест 2


rows(dinamic_log): 1000000 (1 млн. http-запросов к динамике сайта за 3 секунды)
rows(hook_log): 10000 (10 000 легитимных запросов от пользователей за 3 секунды)

# php /root/scripts/php/imgtest/ddos_hook.php /tmp/d2.log /tmp/h2.log 5 300 3
LOAD DATA time elapsed: 2.878 sec.
LOAD DATA time elapsed: 0.023 sec.
select time elapsed: 0.501 sec.
rows(ban): 12402
full cicle time elapsed: 3.54 sec.

P.S.
Описанное в статье решение является экспериментальным. Используйте на свой страх и риск
Tags:
Hubs:
Total votes 113: ↑71 and ↓42+29
Comments109

Articles