Как мы боролись с парсерами

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;
}

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;
    }
}



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

© Habrahabr.ru