image
Ключевые моменты:
* Реализация скрипта для проверки PTR посетителей;
* Конфигурирование nginx в IfIsEvil-style с ветвлениями map;
* Имена location в переменных map;
* Управление ветвлением через try_files /nonexist $map_var.

Многие высоконагруженные и популярные сайты страдают от того, что кроме живых посетителей их посещают разнообразные парсеры, боты и прочие автоматические сканеры, которые не несут никакого полезного эффекта, а только создают паразитный трафик и нагрузку на, и без того, нагруженную систему. В данном случае я не имею виду поисковых ботов, которые хоть и зачастую нагружают проект не нормировано, но просто необходимы любому проекту.
Один из наших клиентов регулярно испытывал проблему лавинообразного роста нагрузки в определенное время суток. Периодически, раз в сутки и чаще происходили наплывы посещений со значительным ростом LA на серверах. Было принято решение построить защиту от паразитного трафика.



Мы обнаружили, что у паразитного трафика имеются определенные паттерны поведения и свойства, а именно:

* По источнику запросов – подсети Amazon, Tor;
* По точкам входа – большинство запросов к разделу товаров;
* По UserAgent – основная часть ботов отправляли UA поисковиков Google, Yandex, Bing, но объективно не являлись ими;
* По рефереру – в основном реферер был пустой.

Реализованные проверки, ограничения и блокировки:

* Вручную добавили заранее известные IP в белый список
* И ранее скомпрометированные IP – в черный список
* Ограничение limit_req_zone для всех, кроме белых списков
* Проверка посетителей с UA поисковиков на соответствие PTR-записи настоящим PTR-записям поисковиков и помещаем в белый список прошедших проверку и всегда разрешаем их.
* При повышении LA порога «Атака»:
** Не прошедших проверку UA по PTR мы заносим в черный список и блокируем.
** Проверяем повторяемость рефереров в access-логе и блокируем посетителей с реферером, превысившим заданное количество вхождений
** Остальных проверяем капчей и разрешаем в случае успеха.

Первые три пункта это распространенные решения, поэтому подробнее остановлюсь на на проверке посетителей по PTR, конфигурировании nginx c использованием конструкции try_files /nonexist $map_var и сложных map.

Мы реализовали скрипт для асинхронной проверки посетителей с UA поисковиков на соответствие PTR-записи настоящим PTR-записям поисковиков.


Он запускается по cron раз в минуту. По уникализированному списку IP посетителей с UA поисковика производит проверку PTR и сверяет доменное имя второго уровня. Если домен совпадает, то добавляет IP в белый список, иначе в черный список. При проверке списка, скрипт не проверяет PTR уже проверенных ранее IP для ускорения процесса проверки. Это позволяет проходить по списку IP из access-лога ежеминутно даже при высокой скорости наполнения access-лога. Записи в черный список заносятся с указанием фиксированного времени удаления из списка блокировки для исключения постоянной блокировки общих IP в больших NAT сетях.

Таким образом мы формируем и поддерживаем файлы ptr_blacklist.map и ptr_whitelist.map которые инклудятся в конфиг nginx.

Запускается ежеминутно.
Листинг скрипта проверки соответствия UA и PTR:
#!/bin/bash

# Базовые настройки скрипта, обычно выносятся в отдельный файл,
# чтобы их можно было подстраивать под конкретный проект, не меняя основной скрипт.
# Export inc file for nginx
EXPORT_MAP=true

# Domain list
DOMAIN_LIST="domain"

# Block time (in minutes)
BLOCK_TIME=1440

# White list IP
IP_WHITELIST=""

# White list PTR
BOTS="google|yandex|bing|Bing|msn"

# false - not block IP if there is a PTR record
BLOCK_WITH_PTR=true

UNBLOCK_ENABLE=true
LOGFILE=/var/log/ua-table.log
LOGFILE2=/var/log/ua-table-history.log
LOCK=/tmp/ua_check.lock

D=$DOMAIN_LIST

# Скрипт формирует ptr_blacklist и ptr_whitelist и только потом копирует их в map-файлы
# для минимизации блокировки рабочих файлов
BL_FILE=/etc/nginx/vhosts.d/ptr_blacklist
WL_FILE=/etc/nginx/vhosts.d/ptr_whitelist
BL_FILE_MAP=$BL_FILE.map
WL_FILE_MAP=$WL_FILE.map

TMP_LOG=/tmp/$D-acc-temp.log
TMP_LOG1=/tmp/$D-acc-temp1.log
NGINX_LOG=/srv/www/$D/shared/log/$D-acc.log

[ ! -f /usr/bin/host ] && echo "/usr/bin/host not found. Please yum install bind-utils" && exit
[ -z "$DOMAIN_LIST" ] && echo "DOMAIN_LIST is empty"
[ ! -f $LOGFILE ] && touch $LOGFILE
[ ! -f $LOGFILE2 ] && touch $LOGFILE2

debug="0"

function e {
    echo -e $(/bin/date "+%F %T") $1
}

# Проверяем не запущен ли скрипт, это позволяет не дублировать затянувшиеся проверки
[ -f $LOCK ] && e "Script $0 is already runing" && exit
/bin/touch $LOCK

DT=`/bin/date "+%F %T"`

if [ ! -f $NGINX_LOG ];then
    echo "Log ($NGINX_LOG) not found."
    /bin/rm -rf $LOCK
    exit
fi

# Основная часть скрипта

# Делаем выборку из acc-лога, регистр важен, так мы не цепляем записи с вхождением в referer
/bin/egrep "Yandex|Google|bingbot|Bing" $NGINX_LOG | /usr/bin/awk '{print $1}' | /bin/sort -n | /usr/bin/uniq > $TMP_LOG


if [ "$EXPORT_MAP" == "true" ]; then
    [ ! -f $BL_FILE_MAP ] && /bin/touch $BL_FILE_MAP
    [ ! -f $WL_FILE_MAP ] && /bin/touch $WL_FILE_MAP
    [ ! -f $BL_FILE ] && /bin/touch $BL_FILE || /bin/cp -f $BL_FILE $BL_FILE.bak
    [ ! -f $WL_FILE ] && /bin/touch $WL_FILE || /bin/cp -f $WL_FILE $WL_FILE.bak
fi

# Разблокируем адреса
UNBLOCK=0
while read line
do
    if [[ "$line" == *=* ]]; then
        GET_TIME=`echo $line | /usr/bin/awk -F"=" '{print $2}'`
        NOW=`/bin/date '+%s'`
        #echo $NOW
        #echo $GET_TIME
        if [ "$NOW" -gt "$GET_TIME" ]; then
            IP=`echo $line | awk '{print $3}'`
            e "$IP unblocked." >> $LOGFILE2
            /bin/sed -i '/'$IP'/d' $BL_FILE
            /bin/sed -i '/'$IP'/d' $LOGFILE
            UNBLOCK=1
        #else
            #e "Nothing to unblock" >> $LOGFILE2
       fi
    fi
done < $LOGFILE

# Блокируем адреса
while read line
do
    IP=$line
    wl=0
    bl=0

# входит в ручной WL
    for I in $IP_WHITELIST
    do
        if [ "$I" = "$IP" ];then
            wl=1
        fi
    done

# ранее уже проверен и внесен в WL
    for I in $(/usr/bin/awk '{print $1}' < "$WL_FILE" )
    do
        if [ "$I" = "$IP" ];then
            wl=1
        fi
    done

# ранее уже проверен и внесен в BL
    for I in $(/usr/bin/awk '{print $1}' < "$BL_FILE" )
    do
        if [ "$I" = "$IP" ];then
            bl=1
        fi
    done

# Если IP есть в списках, значит ранее уже проверен, не проверяем его PTR
    if [ "$wl" = "1" -o "$bl" = "1" ]; then
       [ "$debug" -gt "1" ] && e "$IP in white or black list" >> $LOGFILE2
    else
        PTR=""
        SRCHBOT=""
        FINDPTR="`/usr/bin/host $IP | /bin/grep -v 'not found' | /bin/grep -v 'no PTR record' | /usr/bin/head -1 | /usr/bin/awk '{ print $5 }' | /bin/sed 's/\.$//'`"
        if [ -z "$FINDPTR" ];then
            PTR=" (PTR record not found)"
        else
            PTR=" ($FINDPTR)"
        fi
        SRCHBOT=`/usr/bin/host $IP | /usr/bin/awk '{ print $5 }' | /usr/bin/rev | /usr/bin/cut -d . -f 2-3 | /usr/bin/rev | /bin/egrep "$BOTS"`
        [ -n "$SRCHBOT" ] && BOT="YES" || BOT="NO"
        [ -z "$BLOCK_WITH_PTR" ] && BLOCK_WITH_PTR=true
        if [ "$EXPORT_MAP" == "true" ]; then
            if [ "$BOT" == "NO" ]; then
                e "$IP blocked $BLOCK_TIME minutes. ($D) Unblock = `/bin/date --date="$BLOCK_TIME minute" +%s`" >> $LOGFILE
                e "$IP$PTR blocked $BLOCK_TIME minutes. ($D)" >> $LOGFILE2
                echo "$IP 0;" >> $BL_FILE
            else
                echo "$IP 1;" >> $WL_FILE
            fi
        fi
    fi
done < $TMP_LOG

# часть проверки и подмены map-файлов
if [ "$EXPORT_MAP" == "true" ]; then

    /bin/sort -u -o $BL_FILE $BL_FILE > /dev/null 2>&1
    /bin/sort -u -o $WL_FILE $WL_FILE > /dev/null 2>&1
    
    MAP_CHANGED=0
    if ! diff $BL_FILE $BL_FILE.bak > /dev/null 2>&1; then
        /bin/cp -f $BL_FILE_MAP $BL_FILE_MAP.bak > /dev/null 2>&1
        /bin/cp -f $BL_FILE $BL_FILE_MAP > /dev/null 2>&1
        MAP_CHANGED=1
    fi
    if ! diff $WL_FILE $WL_FILE.bak > /dev/null 2>&1; then
        /bin/cp -f $WL_FILE_MAP $WL_FILE_MAP.bak > /dev/null 2>&1
        /bin/cp -f $WL_FILE $WL_FILE_MAP > /dev/null 2>&1
        MAP_CHANGED=1
    fi
    if [ "$MAP_CHANGED" -eq "1" -o "$UNBLOCK" -eq "1" ]; then
	RELOAD=`/usr/sbin/nginx -t 2>&1 | /bin/grep ok`
	if [ -n "$RELOAD" ];then
   	    /sbin/service nginx reload
            e "nginx is reloaded" >> $LOGFILE2
	else
    	    ERROR_RELOAD=`/sbin/service nginx configtest 2>&1`
	    /bin/cp -f $BL_FILE_MAP.bak $BL_FILE_MAP > /dev/null 2>&1
	    /bin/cp -f $WL_FILE_MAP.bak $WL_FILE_MAP > /dev/null 2>&1
            e "nginx error config test failed" >> $LOGFILE2
	fi

    fi
fi
/bin/rm -rf $LOCK


Скрипт проверки частоты рефереров и формирование файла referer-block.conf вида:

~domain.ru 0;
~… 1;
~… 1;

Запускается ежеминутно.
Листинг скрипта проверки частоты рефереров:
#!/bin/bash
# referer_protect v.1.0.6

# Базовые настройки скрипта, обычно выносятся в отдельный файл,
# чтобы их можно было подстраивать под конкретный проект, не меняя основной скрипт.
RECORDS=500
DOMAIN_LIST=domain
LA=15 # if Load Average > $LA = Referer is block
BLOCK_TIME=360 #in minutes
#REF_WHITELIST=""
BLOCK_ENABLE=true # true/false - enable/disable add firewall rule.
email="mail@mail.ru"
LOGFILE=/var/log/referer-table.log
LOGFILE2=/var/log/referer-table-history.log
LOCK=/tmp/referer.lock
MSG_ALERT=/tmp/msg-alert.tmp
debug="0"

LA_CURRENT="`cat /proc/loadavg | awk '{ print $1}' | awk 'BEGIN { FS="."; }{ print $1}'`"
DT=`date "+%F %T"`

[ ! -f $LOGFILE ] && touch $LOGFILE
[ -f "$MSG_ALERT" ] && rm -f $MSG_ALERT

function e {
    echo -e $(date "+%F %T") $1
}

function msg {
    echo "Referer:$REFERER. Domain:$D" >> $MSG_ALERT
}

function send_mail {
    if [ "$BLOCK_ENABLE" = "true" -a "$LA_CURRENT" -gt "$LA" ];then
	cat $MSG_ALERT | mailx -s "Referers report. Warning" $email
    elif [ "$BLOCK_ENABLE" = "true" -a "$LA_CURRENT" -le "$LA" ];then
	cat $MSG_ALERT | mailx -s "Referers report. Notice " $email
    else
	cat $MSG_ALERT | mailx -s "Referers report. Notice (Test mode)" $email
    fi
}

[ -f $LOCK ] && e "Script $0 is already runing" && exit
touch $LOCK

NEED_NGINX_RELOAD=0

for D in $DOMAIN_LIST
do

    TMP_LOG=/tmp/ddos-$D-acc-referer.log
    TMP_AWK=/tmp/tmp_$D-awk.tmp
    #NGINX_LOG=/srv/www/$D/logs/$D-acc
    NGINX_LOG=/srv/www/$D/shared/log/$D-acc.log
    REFCONF=/etc/nginx/referer-block-$D.conf

    [ ! -s "$REFCONF" ] && echo "~$D 0;" >> $REFCONF

    if [ ! -f $NGINX_LOG ];then
        echo "Log ($NGINX_LOG) not found."
        /bin/rm -rf $LOCK
        exit
    fi

    tail -10000 $NGINX_LOG | awk '($9 == "200") || ($9 == "404")' | awk '{print $11}' | sort | uniq -c | sort -n | awk -v x=$RECORDS ' $1 > x {print $2} ' > $TMP_LOG
    sed -i "s/\"//g" $TMP_LOG # убираем кавычки
    sed -i "/^-/d" $TMP_LOG # убираем referer "-"
    sed -i "/$D/d" $TMP_LOG # убираем свой домен
    sed -i "/^localhost/d" $TMP_LOG # убираем localhost
    awk -F/ '{print $3}' $TMP_LOG > $TMP_AWK # оставляем только домен от url
    cat $TMP_AWK > $TMP_LOG

    # Разблокируем заблокированных referer
    while read line
        do
            if [[ "$line" == *=* ]]; then
                GET_TIME=`echo $line | awk -F"=" '{print $2}'`
                NOW=`date +%s`
                #echo $NOW
                #echo $GET_TIME
                if [ "$NOW" -gt "$GET_TIME" ]; then
                    REFERER=`echo $line | awk '{print $4}'`
                    e "Referer $REFERER unblocked." >> $LOGFILE2
                    /bin/sed -i '/'$REFERER'/d' $LOGFILE
                    /bin/sed -i '/'$REFERER'/d' $REFCONF
		    NEED_NGINX_RELOAD=1
               fi
            fi
        done < $LOGFILE

    # Блокируем referer
    while read line
    do
        REFERER=$line

        DOUBLE=`cat $REFCONF | grep "$REFERER"`
        if [ -n "$DOUBLE" ]; then
            [ "$debug" != "0" ] && e "referer $REFERER exist in DROP rule"
	else
	    if [ "$BLOCK_ENABLE" = "true" -a "$LA_CURRENT" -gt "$LA" ];then
		echo "~$REFERER 1;" >> $REFCONF
		e "Referer $REFERER blocked $BLOCK_TIME minutes ($D) Unblock = `date --date="$BLOCK_TIME minute" +%s`" >> $LOGFILE
		e "Referer $REFERER blocked $BLOCK_TIME minutes ($D)" >> $LOGFILE2
	        NEED_NGINX_RELOAD=1
		if [ ! -s "$MSG_ALERT" ];then 
		    echo "Status: WARNING" > $MSG_ALERT
		    echo "Date: $DT" >> $MSG_ALERT
		    echo "Referer: $RECORDS matches from 10000" >> $MSG_ALERT 
		    echo "LA: $LA_CURRENT" >> $MSG_ALERT
		    echo "Referer(s) is blocked on $BLOCK_TIME minutes:" >> $MSG_ALERT
		    echo "" >> $MSG_ALERT
		fi
		msg
	    elif [ "$BLOCK_ENABLE" = "true" -a "$LA_CURRENT" -le "$LA" ];then
    		TESTDOUBLE=`cat $LOGFILE | grep "$REFERER"`
	        if [ -z "$TESTDOUBLE" ]; then
		    e "Referer $REFERER TEST blocked $BLOCK_TIME minutes ($D) Unblock = `date --date="$BLOCK_TIME minute" +%s`" >> $LOGFILE
		    e "TEST. Referer $REFERER TEST blocked $BLOCK_TIME minutes ($D)" >> $LOGFILE2
		    if [ ! -s "$MSG_ALERT" ];then 
			echo "Status: Notice" > $MSG_ALERT
			echo "Date: $DT" >> $MSG_ALERT
			echo "Referer: $RECORDS matches from 10000" >> $MSG_ALERT 
			echo "LA: $LA_CURRENT" >> $MSG_ALERT
			echo "Referer(s) not blocking:" >> $MSG_ALERT
			echo "" >> $MSG_ALERT
		    fi
		    msg
		fi
	    else
    		TESTDOUBLE=`cat $LOGFILE | grep "$REFERER"`
	        if [ -z "$TESTDOUBLE" ]; then
		    e "Referer $REFERER TEST blocked $BLOCK_TIME minutes ($D) Unblock = `date --date="$BLOCK_TIME minute" +%s`" >> $LOGFILE
		    e "TEST. Referer $REFERER TEST blocked $BLOCK_TIME minutes ($D)" >> $LOGFILE2
		    if [ ! -s "$MSG_ALERT" ];then 
			echo "Date: $DT" > $MSG_ALERT
			echo "Current referer found over $RECORDS matches from 10000 records, but script working is TEST MODE " >> $MSG_ALERT 
			echo "Current LA - $LA_CURRENT" >> $MSG_ALERT
			echo "Referer(s) not blocking:" >> $MSG_ALERT
			echo "" >> $MSG_ALERT
		    fi
		    msg
		fi
	    fi
	fi

    done < $TMP_LOG

[ -n "email" -a -s "$MSG_ALERT" ] && send_mail

done

# reload nginx if config change
if [ $NEED_NGINX_RELOAD -eq 1 ]; then
  /sbin/service nginx reload >/dev/null 2>/dev/null
fi

/bin/rm -rf $LOCK


Конфигурационный файл nginx как симлинк указывает на один из двух файлов, работающих в обычном и high LA режимах.

Режим переключается скриптом, исполняемым ежеминутно из Cron
Скрипт переключения режимов:
#!/bin/bash

### check LA level
MAX_LA=10

processid=`/sbin/pidof -x $(basename $0) -o %PPID`
if [[ $processid ]];then
exit
fi

CFG_DDOS='fpm.domain.ru.ddos'
CFG_NODDOS='fpm.domain.ru.noddos'


load_average=$(uptime | awk '{print $11}' | cut -d "." -f 1)
echo "$(date '+%Y-%m-%d %H:%M') : LA $load_average"

if [[ $load_average -ge $MAX_LA ]]; then
  if [ -f /tmp/la_flag ]; then
    date '+%s' > /tmp/la_flag 
    exit 1
  else
#    echo "$(date +%Y-%m-%d-%H-%M)"
    date '+%s' > /tmp/la_flag 
    mv /etc/nginx/vhosts.d/new.domain.ru.conf /etc/nginx/vhosts.d/new.domain.ru.conf.bak > /dev/null 2>&1
    ln -s /etc/nginx/vhosts.d/$CFG_DDOS /etc/nginx/vhosts.d/new.domain.ru.conf
    reload=`/usr/sbin/nginx -t 2>&1 | grep ok`
    if [ -n "$reload" ];then
      /sbin/service nginx reload
      rm -f /etc/nginx/vhosts.d/new.domain.ru.conf.bak > /dev/null 2>&1
      echo "$(date '+%Y-%m-%d %H:%M') : DDOS config up $reload"
      exit 0
    else
      /sbin/service nginx configtest 2>&1
      mv /etc/nginx/vhosts.d/new.domain.ru.conf.bak /etc/nginx/vhosts.d/new.domain.ru.conf > /dev/null 2>&1
      echo "nginx error config ddos test failed"
      echo "alarm nginx config ddos test failed" | mail -s alarm root		
      exit 1
    fi
  fi
else
  if [ -f /tmp/la_flag ]; then
    TIMEA=`cat /tmp/la_flag`
    TIMEC=`date '+%s'`
    TIMED=$(( $TIMEC - $TIMEA ))
    if [ $TIMED -gt 600 ]; then
      echo "high LA ENDED $(date +%Y-%m-%d-%H-%M)"
      rm -f /tmp/la_flag > /dev/null 2>&1
      mv /etc/nginx/vhosts.d/new.domain.ru.conf /etc/nginx/vhosts.d/new.domain.ru.conf.bak > /dev/null 2>&1
      ln -s /etc/nginx/vhosts.d/$CFG_NODDOS /etc/nginx/vhosts.d/new.domain.ru.conf
      reload=`/usr/sbin/nginx -t 2>&1 | grep ok`
      echo "$(date '+%Y-%m-%d %H:%M') : NO DDOS config up $reload"
      if [ -n "$reload" ];then
         /sbin/service nginx reload
         rm -f /etc/nginx/vhosts.d/new.domain.ru.conf.bak > /dev/null 2>&1
         echo "$(date '+%Y-%m-%d %H:%M') : NO ddos config up"
	 exit 0
      else
        /sbin/service nginx configtest 2>&1
        mv /etc/nginx/vhosts.d/new.domain.ru.conf.bak /etc/nginx/vhosts.d/new.domain.ru.conf > /dev/null 2>&1
	echo "nginx error config noddos test failed"
	echo "alarm nginx config noddos test failed" | mail -s alarm root
        exit 1
      fi
    else
      exit 1
    fi
  else
    exit 1
  fi
fi 


Часть конфигурации вынесена в отдельный файл, подключаемый в основных конфигурационных файлах.

Файл с общими параметрами конфигурации для обоих режимов vhosts.d/map.domain.ru.inc:
map_hash_bucket_size 128;
geoip_country /usr/share/GeoIP/GeoIP.dat;

limit_req_zone $newlimit_addres1 zone=newone:10m rate=50r/m;

map $whitelist-$remote_addr:$remote_port $newlimit_addres1 {
    ~"^0"                   $binary_remote_addr;
    ~"^1-(?<match_rap>.*)"  $match_rap;
}

geo $whitelist {
   default 0;
   91.205.47.150 1;
   194.87.91.154 1;
   83.69.225.78 1;
   77.88.18.82 1;
   91.143.46.202 1;
   213.180.192.0/19 1;
   87.250.224.0/19 1;
   77.88.0.0/18 1;
   93.158.128.0/18 1;
   95.108.128.0/17 1;
   178.154.128.0/17 1;
   199.36.240.0/22 1;
   84.201.128.0/18 1;
   141.8.128.0/18 1;
   188.134.88.105 1;
   89.163.3.25 1;
   46.39.246.91 1;
   84.21.76.123 1;
   136.243.83.53 1;
   77.50.238.152 1;
   83.167.117.49 1;
   109.188.82.40 1;
   79.141.227.19 1;
   176.192.62.78 1;
   86.62.91.133 1;
   144.76.88.101 1;
}

# блокировка рефереров через скрипт block_referer.sh
map $http_referer $bad_referer {
    default      "0";
    include /etc/nginx/referer-block.conf;
}
map $http_referer:$request_method $bad_post_referer {
    default      "0";
    "~*domain.ru.*:POST$" "0";
    "~*:POST$" "1";
    include /etc/nginx/referer-block.conf;
}

# Некоторый набор спицефичных блокировок проекта
map $query_string $bad_query {
...
    default 0;
}

# проверка кук, которые устанавливают в файле /checkcapcha.php
map $http_cookie $allowed_cookie {
  "~somecookie" 1;
  default  0;
}

Ограничение по GeoIP в режиме Под атакой
map $geoip_country_code $allowed_country {
    RU 1;
    default 0;
}

# блокировка подсетей Amazon
include vhosts.d/deny-amazon.inc;

# Ручные белый и черный списки
map $remote_addr $valid_addr {
    include vhosts.d/main_blacklist.map;
    include vhosts.d/main_whitelist.map;
    default 2;
}

# UA посетителей-ботов
map $http_user_agent $user_agent_search_bot {
    "~Yandex"           "1";
    "~Google"           "1";
    "~*bing"            "1";
    "~*MSNBot"          "1";
    default             "";
}

map $remote_addr $ptr_wl_bl {
    include vhosts.d/ptr_blacklist.map;
    include vhosts.d/ptr_whitelist.map;
    default "";
}
map "$user_agent_search_bot:$ptr_wl_bl" $searchbot {
    "1:1"  "1";
    "1:0"  "0";
    default  "2";
}


Листинги конфигурационных файлов

Основной конфиг для нормального режима работы vhosts.d/fpm.domain.ru.noddos:
include vhosts.d/map.domain.ru.inc;

map "$searchbot:$valid_addr:$bad_referer:$bad_query" $root_location_p1 {
    default   @allow_limit;
    "~^1:"    @allow;
    "~^2:1"   @allow_limit;

    "~^0"      @loc_403;
    "~^2:0"    @loc_403;
    "2:2:1:0"  @loc_403;
    "2:2:1:1"  @loc_403;
    "2:2:0:1"  @loc_403;
}

map "$searchbot:$valid_addr:$bad_post_referer:$bad_query" $root_only_location_p1 {
    default   @allow_limit;
    "~^1:"    @allow;
    "~^2:1"   @allow_limit;

    "~^0"      @loc_403;
    "~^2:0"    @loc_403;
    "2:2:1:0"  @loc_403;
    "2:2:1:1"  @loc_403;
    "2:2:0:1"  @loc_403;
}

########################################################

server {

    listen 80;
    listen 443 ssl;

    fastcgi_read_timeout 300s;
    fastcgi_send_timeout 300s;
    fastcgi_connect_timeout 300s;

    server_name domain.ru www.domain.ru m.domain.ru www.m.domain.ru; 

    ssl_certificate ssl/www.domain.ru.crt;
    ssl_certificate_key ssl/www.domain.ru.key;
    charset UTF-8;

    access_log /srv/www/domain/shared/log/domain-acc.log main;
    error_log /srv/www/domain/shared/log/domain-err.log;

    root   /srv/www/domain/current/public/;

    error_page 500 502 /highla.html;

# Выдается capcha в фарме POST с action="/checkcapcha.php"
    location = /highla.html {
        charset UTF-8;
        root   /srv/www/domain/current/public/;
        allow all;
    }

# Устанавливается хэшированная кука на базе адреса посетителя.
    location = /checkcapcha.php {
        charset UTF-8;
        root   /srv/www/domain/current/public/;
        include fastcgi_params;
        fastcgi_buffers 8 16k;
        fastcgi_buffer_size 32k;
        fastcgi_index index.php;
        fastcgi_param  SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param  REQUEST_SCHEME     $scheme;
        fastcgi_param  HTTPS              $https if_not_empty;
        fastcgi_pass 127.0.0.1:9000;
        allow all;
    }

# Именованные location для ветвления по ним через переменную map
    location @loc_403 {
      access_log /srv/www/domain/shared/log/loc_403-acc main;
      return 403;
    }

    location @allow {
        access_log /srv/www/domain/shared/log/allow-acc main;
        add_header X-debug-message "Allow";
        try_files $uri /index.php?$query_string;
    }

    location @allow_limit {
        limit_req zone=newone burst=15;
        access_log /srv/www/domain/shared/log/allow-acc main;
        add_header X-debug-message "Allow";
        try_files $uri /index.php?$query_string;
    }

    location @deny {
        access_log /srv/www/domain/shared/log/deny-acc main;
        add_header X-debug-message "Deny";
        return 403;
    }
    location @restrict {
        access_log /srv/www/domain/shared/log/resrtict-acc main;
        add_header X-debug-message "Restrict";
        return 502;
    }

    location / {
        try_files /fake-nonexistens-location-forr273 $root_location_p1;
    }

    location = / {
        try_files /fake-nonexistens-location-forr273 $root_only_location_p1;
    }

    location ~* \.php {
       include fastcgi_params;
       fastcgi_buffers 8 16k;
       fastcgi_buffer_size 32k;
       fastcgi_index index.php;
       fastcgi_param  SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
       fastcgi_param  REQUEST_SCHEME     $scheme;
       fastcgi_param  HTTPS              $https if_not_empty;
       fastcgi_pass 127.0.0.1:9000;
    }

    location ~* \.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|tar|mid|midi|wav|bmp|rtf|js|swf|flv|avi|djvu|mp3)$ {
        root /srv/www/domain/current/public;
	expires 7d;
	access_log off;
	log_not_found off;
    }

    location ~ /\.git {
        deny all;
    }

    location ~ /\.ht {
        deny all;
    }

    location ~ /\.svn {
        deny all;
    } 
}


Конфиг для режима high LA vhosts.d/fpm.domain.ru.ddos:
include vhosts.d/map.domain.ru.inc;

map "$searchbot:$valid_addr:$bad_referer:$bad_query" $root_location {
    default   @main;
    "~^1:"    @allow;
    "~^2:1"   @allow;

    "~^0"      @loc_403;
    "~^2:0"    @loc_403;
    "2:2:1:0"  @loc_403;
    "2:2:1:1"  @loc_403;
    "2:2:0:1"  @loc_403;
}

map "$searchbot:$valid_addr:$bad_post_referer:$bad_query" $root_only_location {
    default   @main;
    "~^1:"    @allow;
    "~^2:1"   @allow;

    "~^0"      @loc_403;
    "~^2:0"    @loc_403;
    "2:2:1:0"  @loc_403;
    "2:2:1:1"  @loc_403;
    "2:2:0:1"  @loc_403;
}

map "$allowed_country:$allowed_cookie" $main_location {
    "1:0"    @allow_limit;
    "1:1"    @allow_limit;
    "0:1"    @allow_limit;
    default  @restrict;
}

########################################################

server {
    listen 80;
    listen 443 ssl;
    
    fastcgi_read_timeout 300s;
    fastcgi_send_timeout 300s;
    fastcgi_connect_timeout 300s;

    server_name domain.ru www.domain.ru m.domain.ru www.m.domain.ru;

    ssl_certificate ssl/www.domain.ru.crt;
    ssl_certificate_key ssl/www.domain.ru.key;
    charset UTF-8;

    access_log /srv/www/domain/shared/log/domain-acc.log main;
    error_log /srv/www/domain/shared/log/domain-err.log;

    root   /srv/www/domain/current/public/;

# Выдается capcha в фарме POST с action="/checkcapcha.php"
    error_page 500 502 /highla.html;
    location = /highla.html {
        charset UTF-8;
	root   /srv/www/domain/current/public/;
        allow all;
    }

# Устанавливается хэшированная кука на базе адреса посетителя.
    location = /checkcapcha.php {
        charset UTF-8;
        root   /srv/www/domain/current/public/;
        include fastcgi_params;
        fastcgi_buffers 8 16k;
        fastcgi_buffer_size 32k;
        fastcgi_index index.php;
        fastcgi_param  SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param  REQUEST_SCHEME     $scheme;
        fastcgi_param  HTTPS              $https if_not_empty;
        fastcgi_pass 127.0.0.1:9000;
        allow all;
    }

# Именованные location для ветвления по ним через переменную map
    location @loc_403 {
      access_log /srv/www/domain/shared/log/loc_403-acc main;
      return 403;
    }
    
    location @allow {
        access_log /srv/www/domain/shared/log/allow-acc main;
        add_header X-debug-message "Allow";
        try_files $uri /index.php?$query_string;
    }

    location @allow_limit {
        limit_req zone=newone burst=55;
        access_log /srv/www/domain/shared/log/allow-limit-acc main;
        add_header X-debug-message "Allow";
        try_files $uri /index.php?$query_string;
    }

    location @deny {
        access_log /srv/www/domain/shared/log/deny-acc main;
        add_header X-debug-message "Deny";
        return 403;
    }
    location @restrict {
        access_log /srv/www/domain/shared/log/resrtict-acc main;
        add_header X-debug-message "Restrict";
        return 502;
    }

    location @main {
        add_header X-debug-message "Main";
	try_files /fake-nonexistens-location-forr273 $main_location;
    }

    location / {
	try_files /fake-nonexistens-location-forr273 $root_location;
    }

    location = / {
	try_files /fake-nonexistens-location-forr273 $root_only_location;
    }

    location ~* \.php {
       include fastcgi_params;
       fastcgi_buffers 8 16k;
       fastcgi_buffer_size 32k;
       fastcgi_index index.php;
       fastcgi_param  SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
       fastcgi_param  REQUEST_SCHEME     $scheme;
       fastcgi_param  HTTPS              $https if_not_empty;
       fastcgi_pass 127.0.0.1:9000;
    }

    location ~* \.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|tar|mid|midi|wav|bmp|rtf|js|swf|flv|avi|djvu|mp3)$ {
        root /srv/www/domain/current/public;
        expires 7d;
        access_log off;
        log_not_found off;
    }

    location ~ /\.git {
        deny all;
    }

    location ~ /\.ht {
        deny all;
    }

    location ~ /\.svn {
        deny all;
    }
}


Итог

Этим решением мы помогли нашему клиенту защитить свой проект от паразитного трафика и повысить стабильность работы серверов.
Автор: ведущий системный администратор компании Марат Рахимов.

Комментарии (54)


  1. 1it
    04.03.2016 10:57
    +1

    Т.е. получается что таки каждую минуту может происходить nginx -s reload?
    Мне кажется можно делегировать половину действий самому nginx и обойтись без частых reload.


    1. akhaustov
      04.03.2016 12:53

      Потенциально, может. На практике с таким не сталкивались пока. Да, и решение не претендует на универсальность. Для конкретного проекта хорошо подошло,


  1. gearbox
    04.03.2016 12:55
    +20

    у паразитного трафика имеются определенные паттерны поведения и свойства

    Проблема в том что вы обнаруживаете свойства, заранее вам известные. Попробуйте положить в свои страницы код проверки на наличие пользователя (типа рекапчи от гугля, только не капча а именно проверка на наличие действий пользователя — метание курсора, выделение текста) — и удивитесь сколько вас  фантомом парсят, и при этом в озвученные выше фильтры не попадают.
    А проверка капчей — честно, спорно. Что бы отсечь посещения которые мешают только вам, вы создаете неудобства своим пользователям. За что?


    1. maxru
      04.03.2016 14:28
      +7

      Можно написать webdriver-бота (хотя бы и через selenium), он и мышкой будет дёргать и выделять текст и вообще.
      (зато будут забанены пользователи, которые жабаскрипт отключили)


      1. gearbox
        04.03.2016 15:02
        +1

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


        1. tbicr
          04.03.2016 16:29
          -1

          В общем цена получения данных или содания нагрузки противостоит цене защиты от них с сохранением прибыли.


        1. maxru
          10.03.2016 13:20

          Авторы в любом случае выделили один паттерн из миллиона и используют его.
          Тут вопрос в том, сколько %% ботов они отсекают этим паттерном.


    1. naryl
      04.03.2016 15:07
      +6

      У меня джаваскрипт отключён, а когда включён, я ни мышкой не метаюсь, ни текст не выделяю, т.к. Vimperator.

      UPD: И да, писал парсеры, ни один под приведённый в статье список поведения не попадает, кроме пустого referer'а. Это просто, чтобы подтвердить первую половину Вашей точки зрения.: )

      UPD2: А *большинство* запросов к разделу товаров скорее всего у вообще всех посетителей сайта, если это магазин.


    1. khim
      04.03.2016 16:36
      +9

      У меня такое ощущения сложилось, что задачи "отшить всех ботов" вообще не ставилось.

      Об этом ещё из преамбулы видно: ну стащат что-то боты и стащат, не в этом дело. Хотелось, как я понял, "пришибить" ботов создающих повышенную нагрузку и при этом не дающих ничего взамен (поисковики, понятно, дают и понятно что).

      А CAPTCHA — это, как я понял, от ложных срабатываний: если человека вдруг записали в боты — он разгадает картинку и его пропустят, а для ботоводов есть более простые решения — просто переписать их так, чтобы они не создавали излишней нагрузки...


      1. akhaustov
        04.03.2016 16:38
        +2

        В точку! :)


      1. postgree
        04.03.2016 20:02
        +1

        А для создателей интернетов тогда нужно разработать стандарт файлов, в котором будет лежать вся информация для парсящих. Разрабатывал и парсеры, и системы, которые определяют ботов. Защищаться от ботов себе дороже. Проще переформировывать файл с каталогом товаров (прайс лист).


        1. merlin-vrn
          04.03.2016 20:11
          +3

          Я тоже подумал об этом: ну господи, грузят они, нужна им эта информация, ну так дайте им её простым способом, чтобы им не приходилось парсить. Или даже продайте занедорого.

          Проблема в том, что многие магазины по разным причинам очень не хотят сравнений с конкурентами, а если выдать свой прайс, да ещё в стандартном машиночитаемом формате — сравнения будут на каждом шагу. Поэтому они не захотят делать свои прайсы в таком формате, и парсинг запрещают.


          1. sebres
            05.03.2016 01:43

            для ботоводов с бот-сеткой — фигня вопрос… скажем есть у него в наличии 500 уникальных IP, при сотне запросов в сутки с каждого, ежедневно у него в наличии актуальный прайс лист на 50.000 артиклей… и у конкурентов цена на пару процентов ниже.
            При том, что свои зоопарки ботоводы часто разводят на компах "домохозяек" и т.п. — все эти ip ещё и динамические в добавок… т.е. завтра другие как правило...


  1. vit1251
    04.03.2016 13:12
    +5

    И это все компания делает во время развития семантической концепции интернета.
    Возможно нужно развивать просто защиту информации как-то подписывать контент.
    Или регистрировать себя как первоисточник в тех же поисковых системах.


  1. estin
    04.03.2016 14:16
    +1

    Вы получили временную передышку? Или смогли отвадить "парсеры" совсем?


    1. akhaustov
      04.03.2016 14:42

      От данного клиента отстали. Больше нигде не пробовали применять.


  1. vlreshet
    04.03.2016 14:41
    +3

    ИМХО, это защита только от каких-нибудь общих парсеров, которые не заточены сугубо на ваш сайт. Если у кого-то есть цель парсить именно вас — ничем от этого не защититься


    1. Zibx
      04.03.2016 14:44
      +6

      У них теперь даже есть подробная инструкция по обходу защиты :)


      1. akhaustov
        04.03.2016 14:45

        Нужно только знать, что за клиент ) Из более, чем полутора сотен — сложно угадать.


        1. miwa
          04.03.2016 15:03
          +5

          Если автор парсера читает хабр, ему угадывать не надо :)


  1. jehy
    04.03.2016 15:47
    +24

    Очень российский подход — не знаем, кто и зачем к нам ходит, но давайте на всякий случай запретим. Наверняка это злодеи. Картинка к посту это эпично подтверждает.

    Это же могли быть боты людей, которые занимаются перепродажей товаров вашего клиента, например. Они же не из вредности к вам приходят, а с тем, чтобы сделать что-то полезное для себя и возможно для вас. То, что всплеск наблюдался ежедневно в одно и то же время и на категории товаров это подтверждает — у вас не искали дыры, не ломали вас, не пытались перегрузить сервер — просто тихо-мирно читали информацию.

    Вы могли бы просто поставить лимит на количество запросов с IP адреса в единицу времени — и клиент был бы доволен, и ботоводы бы перенастроили своих ботов, чтобы они аккуратнее ходили на ваш сайт.

    А совсем правильно — ещё вывесить большое объявление о том, что готовы к различного рода партнёрской программе и выгружать данные любым желающим.

    В общем, не удивляйтесь, если у клиента внезапно упадут продажи. Нет, боты не мои, если что.


    1. akhaustov
      04.03.2016 15:56
      -7

      Данная система была согласована с заказчиком и внедрялась по его же просьбе.
      Причиной был постоянный парсинг проекта, что давало бесполезную нагрузку.
      Установка лимитов не давала нужного эффекта.
      Конечно же, никто не будет совершать действий в ущерб бизнесу.
      Считаю, что Ваш комментарий совершенно не в тему.


      1. jehy
        04.03.2016 16:12
        +11

        Комментарий — к тому, что утверждение о том, что нагрузка бесполезна и трафик "паразитарный" — крайне необоснованно.
        Например, вас могут сканировать бот, который предлагает товары по самым низким ценам. В результате бот не сможет парсить ваш сайт, и даже если он продаёт что-то очень хорошее по низкой цене — вы просто вывалитесь из рейтинга.

        Ну и с архитектурной точки зрения решения просто ужасно. Хотя бы по причине постоянной перезагрузки настроек nginx. Когда вы запускаете какой-то процесс сервера (в особенности веб сервер или СУБД), то ожидаете, что он будет работать с изначальными настройками всё время до ручной перезагрузки или ребута. А выходит, что если некий администратор правил настройки — например, хотел подключить SSL — и не доделал их, сохранив в уверенности что вернётся к этому чуть позже, то неверные настройки будут автоматически использованы.

        Если уж заказчик захотел такой функционал, то было правильно использовать подходящие для него средства — например, fail2ban. По айпи адерсам и юзерагентам он отлично умеет определять ботов. Можно было бы так же дописать некие свои проверки — например, сделанную вам проверку по PTR… Хотя кажется, fail2ban умеет и это. Нет, сделали какой-то ужасный костыль и довольны.


        1. akhaustov
          04.03.2016 16:23

          Тут именно по настоянию заказчика занимались реализацией этой задачи. Согласен, что это костыль. fail2ban не помог. Возможно, стоило просто стать под защиту специализированных сервисов. Однако, заказчик настоял именно на таком вот решении. Поверьте, пытались переубедить :)


          1. jehy
            04.03.2016 16:34

            А что не срослось с fail2ban? Верю, что могли быть какие-то интересные грабли, интересно узнать, какие. Сам я fail2ban использовал для защиты ftp/smtp и ещё нескольких сервисов, с http пока не было потребности — но интернет говорит, что используют успешно.


          1. sebres
            04.03.2016 21:49
            +9

            fail2ban не помог

            Ну как бы в мире ИТ не существует подобных словосочетаний — "не работает", "не помогло" (без конкретики).

            А как разработчик fail2ban пока поверю только в то, что установив его (из коробки), вы не получили требуемого результата. Имхо, вполне ожидаемо и закономерно (ибо нужно его "готовить", в принципе как и любой инструмент).

            Точно так-же вы могли бы установить что-нибудь типа spamassassin, и не поправив ни одного конфига, без биндингов (вообще ничего не настраивая), удивляться тому, что до сих пор спам сыплется.

            В результате (вероятно просто не разобравшись) вы сделали велосипед, плохой-хороший не суть важно, ибо все-равно велик. Потому что, если я правильно разобрал, что вы "создали", то как минимум половину работы (если не 2/3) можно было переложить на f2b...


      1. merlin-vrn
        04.03.2016 20:13
        +2

        А вы пытались хотя бы намекнуть клиенту, что, возможно, стоит вместо того, чтобы бороться — возглавить? Сделать api, чтобы желающие мгли получить данные не грузя сайт, без всякого парсинга?


        1. BigD
          04.03.2016 21:38
          -1

          вот-вот. нас просто блокировали без объяснений и попытки связаться.


    1. RomanPyr
      04.03.2016 19:50

      или можно было бы поднять нормальный API на отдельном серваке.


      1. BigD
        04.03.2016 21:39
        +1

        даже за деньги многие не готовы к этому — владельцы сайтов я имею в виду


    1. verydima
      05.03.2016 01:02

      Всё очень правильно написано. Я занимаюсь парсингом более трёх лет. И за 3 года я видел только один сайт, который нельзя было спарсить. Точнее, в заданном промежутке времени я не нашёл решения. Для каждого сайта можно сделать парсер. И он будет эммулировать всё то, что Вы описали в статье и даже больше.
      Наиболее рациональная защита — это ограничение по количеству запросов с 1 ip в единицу времени. Большинство живых людей не будут сидеть и листать Ваш сайт час страницу за страницей. Либо же отдача какой-то части материала. Например, если человек ищет какую-то услугу в каком-то регионе, то ему отдаются не все 40000 результатов поиска, а 3000 первых записей. Для живого человека этого более, чем достаточно (именно так работает таже manta.com).


      1. khim
        05.03.2016 04:40
        +7

        Большинство живых людей не будут сидеть и листать Ваш сайт час страницу за страницей.
        Зато за одним proxy-IP может оказаться вдруг несколько тысяч (если не миллионов) живых людей.


  1. antonwork
    04.03.2016 16:34
    -5

    Сколько эти парсеры давали нагрузки, что "LA значительно рос на серверах". 100 запросов в секунду? 1000? Может проще было оптимизировать код, пересмотреть какие-то подходы, чтобы просто не замечать эту избыточную нагрузку? Ну а если с одного адреса проходит запредельное кол-во запросов на бэкенд мимо кешей, то можно поместить этот адрес в некий список, которому показываю только капчу. (У вас что-то подобное уже реализовано)
    Удивительно, я конечно не знаю всех деталей, но столько вложить сил в латание… по факту костылей.


  1. leMar
    04.03.2016 16:35

    В кои-то веки нормальная техническая статья про конфигурирование, а все комментарии про надо / не надо и не одного про конфиги. На хабре админы остались, вообще?


    1. jehy
      04.03.2016 16:38
      +3

      Чуть выше автор признаёт, что это специфический костыль, обусловленный требованиями заказчика. Ну и не совсем понятно, какие комментарии про конфиги вы хотите услышать.


    1. FeNUMe
      04.03.2016 21:08
      +10

      В статье описан жуткий костыль и она скорее является примером как делать не надо, потому и реакция соответствующая.


  1. questor
    04.03.2016 17:02
    +2

    Статья и тематика спорная, но прочитал с интересом. Не хватает цифр — доля паразитного трафика до и после, какие меры из реализованных больше всего повлияли на итоговую фильтрацию.


  1. rekby
    04.03.2016 18:24

    PTR-записи у многих хостеров ставятся автоматом любые и никто не мешает им поставить ptr-записи google или яндекс и т.п. Поисковики рекомендуют потом еще обратное преобразование проверять.

    Еще может иметь смысл проверять IP-адреса по whois, тогда если whois правильный и соответствует яндексу/гуглу и т.п. — можно в белый список сразу всю их сеть внести и данные в whois подделать сложнее, чем PTR.


  1. simpleadmin
    04.03.2016 18:40
    +2

    geo $whitelist {
    default 0;

    77.88.0.0/18 1;
    93.158.128.0/18 1;
    95.108.128.0/17 1;
    178.154.128.0/17 1;
    199.36.240.0/22 1;

    Не доверяйте просто так подсетям ПС. Они далеко не безгрешны. Парсинг 2-х летней давности с серверов Яндекса:

    178.154.243.106 -       5011415253      1419741951.254  [GET]   [/servis-centr/texet?ah62]      200     15241     -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80    28/Dec/2014:07:45:51 +0300      RU      w:1     []
    178.154.243.105 -       5011428078      1419741951.345  [GET]   [/servis-centr/texet?ah141]     200     241     -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80    28/Dec/2014:07:45:51 +0300      RU      w:1     []
    178.154.243.104 -       5011415952      1419741951.354  [GET]   [/servis-centr/texet?ah88]      200     19249     -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80    28/Dec/2014:07:45:51 +0300      RU      w:1     []
    95.108.158.134  -       5011424897      1419741951.430  [GET]   [/servis-centr/texet?ah69]      200     52415     -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80    28/Dec/2014:07:45:51 +0300      RU      w:1     []
    178.154.243.107 -       5011410458      1419741951.574  [GET]   [/remont-chasov/royal-london18ao]       200     8029    -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80     28/Dec/2014:07:45:51 +0300      RU      w:1     []
    5.255.253.3     -       5011428792      1419741951.586  [GET]   [/remont-chasov/royal-london19au]       200     8030    -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80     28/Dec/2014:07:45:51 +0300      RU      w:1     []
    95.108.128.241  -       5011423471      1419741951.593  [GET]   [/remont-chasov/royal-london22a]        200     8028    -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80     28/Dec/2014:07:45:51 +0300      RU      w:1     []
    95.108.128.242  -       5011427425      1419741951.617  [GET]   [/servis-centr/texet?ah73]      200     15245     -       [Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) YaBrowser/1.0.1084.5402 Chrome/19.0.1084.5409 Safari/536.5]       m*****.ru       XXX.XXX.217.215:80    28/Dec/2014:07:45:51 +0300      RU      w:1     []

    Около 3'000'000 запросов за сутки. Разумеется никакими Яндекс-ботами здесь и не пахнет


  1. reji
    04.03.2016 19:42
    +2

    А что мешало реализовать динамическую роботоловилку с прослойкой перед внутри nginx на lua? Так делает, например, Wallarm.
    По временным затратам вышло бы примерно тоже самое, но скорость и качество работы ни в сравнение.

    Не критики для, а вопроса ради. Решение, как я понимаю, уже протестировано и работает в рамках ТЗ заказчика. Интересует, почему вы пришли именно к такому решению.

    P.S. Соглашусь с комментарием jehy, что можно было не банить, а попытаться связаться с владельцами ботов. Отдавая хотя бы стандартную страницу "call me maybe"


    1. akhaustov
      04.03.2016 19:45

      Мешало отсутствие знаний lua и времени на его изучение. А так да, такая идея была.


      1. werdender
        07.03.2016 18:41
        +1

        Стоило хотя бы попытаться. В него на удивление просто вникнуть.


  1. BigD
    04.03.2016 21:37
    +1

    А мы боремся с теми, кто борется с нашими парсерами… Пока получается. Но мы не такие плохие (даже хорошие я бы сказал) — мы стараемся раскидывать нагрузку (запросы к сайту) в течение дня, а не разово пытаться скачать "весь Интернет".


  1. Melz
    04.03.2016 21:54

    Я иногда пишу парсеры как хобби. Даже у меня есть небольшие наработки по проходу сайтов и ваша защита так себе )
    Лучше сделайте API, если вам кому-то нужны ваши продукты то пускай на них смотрят.

    Если большой пик с одних адресов, то скорее всего идет «синхронизация», т.е. если бот не работал нужно добрать. А так инкремент как правило идет довольно быстро.

    Когда парсерщим это надоесть они купят симку и модем и будут ходить на мобильную версию сайта с сети МТС и тд.
    Мобильная версия накладывает ограничения на всякие капчи, а если забаните ip большой тройки сами плакать будете )


  1. amaksr
    04.03.2016 22:37
    +2

    Мне кажется странным, что интернет-магазин решил ограничить распространение данных из своего раздела Товаров (читай каталогов и прайс-листов). Обычно поступают наоборот. Может лучше было организовать API, а при детектировании бота выдавать ему подсказку с адресом этого API, чтобы облегчить жизнь админу этого бота?


  1. nikitasius
    04.03.2016 22:51
    +5

    Я, чесно, не пойму, какую такую ужасную нагрузку несет nginx динамике c, хотя бы, 30 минутным кешем каталога товаров.
    Ну ей богу, не пойму. При правильной настройке ключей (с cookies и без cookies) у вас отдельные кеши для всех и для зарегистрированных клиентов/редакторов.

    И в таком случае, как правильно заметили выше, роляет рейт запросов на IP. Если динамика тупая и кривая и работает на калькуяторе.\

    Подходит только под: не хочу делиться, хочу быть уникальным! Глупости это все и трата времени. Кто хочет — спарсит. Что не может — попросит того, что может. Я сам, когда парсил нужные мне ресурсы представлялся или хромом, или гуглом, всегда отправлял реферера и еще пачку заголовков, иногда и кукисы поддерживал, и все в 5-6 потоков. Иногда часть через прокси. Мде.


    1. postgree
      05.03.2016 03:15
      +2

      Потому что современные интернет магазины не кешируют ничего даже для анонимов/гостей. Необходим анализ переходов пользователя для отображения спец предложений, похожих или возможно заинтересующих товаров. Маркетологи на крупных проектах сейчас пытаются каждые вероятные 0,5 процента выжать.

      Подходит только под: не хочу делиться, хочу быть уникальным! Глупости это все и трата времени. Кто хочет — спарсит. Что не может — попросит того, что может. Я сам, когда парсил нужные мне ресурсы представлялся или хромом, или гуглом, всегда отправлял реферера и еще пачку заголовков, иногда и кукисы поддерживал, и все в 5-6 потоков. Иногда часть через прокси. Мде.
      Вас можно вычислить. На раз — два. Приходилось использовать вебдрайвер (selenium, PhantomJS), правильно водить мышкой, изучать поведенческие шаблоны и пр. Покупать распознавание капчи наконец. А с учетом стоимости таких услуг иногда только 80$ в день улетало на распознавание капчи.


      1. Blumfontein
        05.03.2016 07:35
        +1

        >> Потому что современные интернет магазины не кешируют ничего даже для анонимов/гостей. Необходим анализ переходов пользователя для отображения спец предложений, похожих или возможно заинтересующих товаров. Маркетологи на крупных проектах сейчас пытаются каждые вероятные 0,5 процента выжать.

        Это все можно на джаваскрипте асинхронно сделать.


      1. merlin-vrn
        05.03.2016 11:06
        +2

        Не на раз-два. Стоимость обнаружения качественного парсера сравнима со стоимостью разработки такого парсера. Короче, тут типичная борьба оружия и брони.

        Только куча комментов в топике намекает, что это глупая борьба, особенно — в данном случае.


      1. nikitasius
        05.03.2016 23:20

        Потому что современные интернет магазины не кешируют ничего даже для анонимов/гостей

        Вы их на мух и котлет разделяете при помощи cookies? 10 секундный кеш по ключу с кукисами решит вашу проблему. Просто добавить $cookiename к ключу.
        Даже для тех, кто не слушает советов, 1 секундный кеш и рейты в каждом случаю 100% решают задачу и коллосально снижают нагрузки. А всю обработку нужно повесить на javascript, как писали ниже. Ну или покупать железо мощнее.

        Вас можно вычислить. На раз — два.

        У вас есть такой сайт (или вы с ним сталкивались), который вычисляет таких парсеров на раз-два? Дайте ссылку в личку, я хочу посмотреть.


  1. torrie
    05.03.2016 08:34
    -1

    Ой, ладно, какая нагрузка? Хотите с ней бороться — помогите парсерам, сделайте api. Или кешируйте данные. Или… Средств борьбы с нагрузкой куча!
    Мне кажется, что вы просто боитесь за данные.


  1. avonar
    05.03.2016 12:28
    +1

    На что только не пойдут люди, лиж бы не сделать файлик с каталогом, в котором все будет лежать в удобоваримой форме.
    Это какая-то борьба с ветряными мельницами, тем более что боты просто парсили контент.
    Впрочем, типично российский подход к решению проблем, все запретить, никого не пускать, пускай клиенты монитор разобьют из-за капчей.


  1. Shablonarium
    05.03.2016 15:27

    Ну да, вместо того чтобы сделать удобный АПИ, давайте объявим потребителей злоумышленниками! Совковый маразм!


  1. sergof
    06.03.2016 19:06
    +2

    Сказ о потраченных человекочасах.