Есть ли способ заставить Nginx уведомлять меня, если количество обращений от реферера превышает пороговое значение?
Например, если мой веб-сайт представлен на Slashdot, и внезапно у меня появляется 2К посещений в час, я хочу получать уведомление, когда превышает 1 КБ посещений в час.
Можно ли будет это сделать в Nginx? Возможно без lua? (поскольку мой продукт не компилируется в lua)
Я думаю, что это было бы намного лучше сделать с помощью logtail и grep. Даже если это можно сделать с помощью встроенного lua, вам не нужны эти накладные расходы для каждого запроса, и вы особенно не хочу этого, когда тебя засекли.
Вот 5-секундная версия. Вставьте его в скрипт и поместите вокруг него более читаемый текст, и вы будете золотыми.
5 * * * * logtail -f /var/log/nginx/access_log -o /tmp/nginx-logtail.offset | grep -c "http://[^ ]slashdot.org"
Конечно, при этом полностью игнорируются reddit.com, facebook.com и все миллионы других сайтов, которые могут отправлять вам много трафика. Не говоря уже о 100 разных сайтах, каждый из которых отправляет вам 20 посетителей. Вам, вероятно, следует просто иметь старый добрый трафик порог, при котором вам будет отправлено электронное письмо, независимо от реферера.
Nginx limit_req_zone Директива может основывать свои зоны на любой переменной, включая $ http_referrer.
http {
limit_req_zone $http_referrer zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
}
Вы также захотите сделать что-то, чтобы ограничить количество состояний, необходимых на веб-сервере, поскольку заголовки реферера могут быть довольно длинными и разнообразными, и вы можете увидеть бесконечное разнообразие. Вы можете использовать nginx split_clients возможность установить переменную для всех запросов, основанную на хэше заголовка реферера. В приведенном ниже примере используется только 10 баксов, но вы можете сделать это и с 1000 так же легко. Таким образом, если у вас есть косая черта, люди, чей реферер попал в ту же корзину, что и URL-адрес slashdot, тоже будут заблокированы, но вы можете ограничить это до 0,1% посетителей, используя 1000 корзин в split_clients.
Это выглядело бы примерно так (полностью непроверено, но строго по направлению):
http {
split_clients $http_referrer $refhash {
10% x01;
10% x02;
10% x03;
10% x04;
10% x05;
10% x06;
10% x07;
10% x08;
10% x09;
* x10;
}
limit_req_zone $refhash zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
}
Наиболее эффективным решением могло бы быть написание демона, который tail -f
в access.log
, и отслеживать $http_referer
поле.
Однако быстрым и грязным решением было бы добавить дополнительный access_log
файл, чтобы регистрировать только $http_referer
переменная с настраиваемым log_format
, и для автоматического поворота журнала каждые X минут.
Это можно сделать с помощью стандартных сценариев logrotate, которым может потребоваться плавный перезапуск nginx для повторного открытия файлов (например, стандартная процедура, взгляните на / a / 15183322 на SO для простого временного сценария)…
Или, используя переменные внутри access_log
, возможно, извлекая подробное описание из $time_iso8601
с помощью map
или if
директива (в зависимости от того, где вы хотите разместить свой access_log
).
Таким образом, у вас может быть 6 файлов журнала, каждый из которых охватывает период в 10 минут, http_referer.Txx{0,1,2,3,4,5}x.log
, например, получая первую цифру минуты, чтобы различать каждый файл.
Теперь все, что вам нужно сделать, это иметь простой сценарий оболочки, который может запускаться каждые 10 минут, cat
все вышеперечисленные файлы вместе, перенаправьте их в sort
, подключите его к uniq -c
, чтобы sort -rn
, чтобы head -16
, и у вас есть список из 16 наиболее распространенных Referer
варианты - вы можете самостоятельно решить, превышает ли какая-либо комбинация чисел и полей ваши критерии, и выполнить уведомление.
Впоследствии, после одного успешного уведомления, вы можете удалить все эти 6 файлов и при последующих запусках не выдавать никаких уведомлений, ЕСЛИ не присутствуют все шесть файлов (и / или определенное другое число, которое вы сочтете нужным).
Да, конечно, в NGINX это возможно!
Что вы могли бы сделать, так это реализовать следующее DFA:
Ограничение скорости агрегата на основе $http_referer
, возможно, используя некоторое регулярное выражение через map
для нормализации значений. Когда лимит превышен, открывается страница внутренней ошибки, которую вы можете поймать с помощью error_page
обработчик как на связанный вопрос, переход в новое внутреннее расположение в качестве внутреннего перенаправления (не видимого для клиента).
В указанном выше месте для превышения лимитов вы выполняете запрос предупреждения, позволяя внешней логике выполнять уведомление; этот запрос впоследствии кэшируется, гарантируя, что вы получите только 1 уникальный запрос в заданное временное окно.
Поймать код состояния HTTP предыдущего запроса (путем возврата кода состояния ≥ 300 и использования proxy_intercept_errors on
или, в качестве альтернативы, используйте не встроенный по умолчанию auth_request
или add_after_body
чтобы сделать «бесплатный» подзапрос) и завершить исходный запрос, как если бы предыдущий шаг не был задействован. Обратите внимание, что нам нужно включить рекурсивный error_page
обработка, чтобы это работало.
Вот мой PoC и MVP, также на https://github.com/cnst/StackOverflow.cnst.nginx.conf/blob/master/sf.432636.detecting-slashdot-effect-in-nginx.conf:
limit_req_zone $http_referer zone=slash:10m rate=1r/m; # XXX: how many req/minute?
server {
listen 2636;
location / {
limit_req zone=slash nodelay;
#limit_req_status 429; #nginx 1.3.15
#error_page 429 = @dot;
error_page 503 = @dot;
proxy_pass http://localhost:2635;
# an outright `return 200` has a higher precedence over the limit
}
recursive_error_pages on;
location @dot {
proxy_pass http://127.0.0.1:2637/?ref=$http_referer;
# if you don't have `resolver`, no URI modification is allowed:
#proxy_pass http://localhost:2637;
proxy_intercept_errors on;
error_page 429 = @slash;
}
location @slash {
# XXX: placeholder for your content:
return 200 "$uri: we're too fast!\n";
}
}
server {
listen 2635;
# XXX: placeholder for your content:
return 200 "$uri: going steady\n";
}
proxy_cache_path /tmp/nginx/slashdotted inactive=1h
max_size=64m keys_zone=slashdotted:10m;
server {
# we need to flip the 200 status into the one >=300, so that
# we can then catch it through proxy_intercept_errors above
listen 2637;
error_page 429 @/.;
return 429;
location @/. {
proxy_cache slashdotted;
proxy_cache_valid 200 60s; # XXX: how often to get notifications?
proxy_pass http://localhost:2638;
}
}
server {
# IRL this would be an actual script, or
# a proxy_pass redirect to an HTTP to SMS or SMTP gateway
listen 2638;
return 200 authorities_alerted\n;
}
Обратите внимание, что это работает, как ожидалось:
% sh -c 'rm /tmp/slashdotted.nginx/*; mkdir /tmp/slashdotted.nginx; nginx -s reload; for i in 1 2 3; do curl -H "Referer: test" localhost:2636; sleep 2; done; tail /var/log/nginx/access.log'
/: going steady
/: we're too fast!
/: we're too fast!
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.1" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.0" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 200 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
%
Вы можете видеть, что первый запрос приводит к одному обращению к интерфейсу и одному обращению к бэкэнду, как и ожидалось (мне пришлось добавить фиктивный бэкэнд в место, где limit_req
, потому что return 200
будет иметь приоритет над пределами, реальный бэкэнд не нужен для остальной обработки).
Второй запрос превышает лимит, поэтому мы отправляем оповещение (получение 200
) и кэшировать его, возвращая 429
(это необходимо из-за вышеупомянутого ограничения, заключающегося в том, что запросы ниже 300 не могут быть перехвачены), который впоследствии перехватывается интерфейсом, который теперь свободен делать все, что захочет.
Третий запрос все еще превышает лимит, но мы уже отправили предупреждение, поэтому новое предупреждение не отправляется.