Wykrywanie efektu Slashdot w nginx

10

Czy jest sposób, aby zmusić Nginx do powiadamiania mnie, jeśli trafienia od strony odsyłającej przekroczą próg?

np. jeśli moja strona internetowa jest prezentowana na Slashdot i nagle mam godzinę 2 000 wyświetleń w ciągu godziny, chcę otrzymywać powiadomienia, gdy liczba wyświetleń przekroczy 1 000 godzin.

Czy będzie to możliwe w Nginx? Być może bez lua? (ponieważ mój prod nie jest skompilowany lua)

Quintin Par
źródło
4
Co to jest „Slashdot”?
ewwhite
Zrobiłem coś takiego, aby wykryć ddos ​​na ngix. Osiągnąłem to, analizując dziennik dostępu. Zrobiłem zadanie crona, aby przeanalizować dziennik dostępu i policzyć unikalne połączenia ip na godzinę.
Heks
8
Masz na myśli, że chcesz, aby Nginx mógł wykryć, czy zostałeś kupiony przez Dice?
MDMarra
1
@ Hex That (i może kilka fragmentów skryptu) stanowiłoby doskonałą odpowiedź na to pytanie :)
voretaq7
3
Prawdopodobnie nie musisz się już martwić o Slashdotted. Twój serwer powinien być w stanie obsłużyć dodatkowe 4 połączenia na godzinę. Może jednak chcesz się martwić, że zostaniesz Redditowany ...
Beznadziejny

Odpowiedzi:

3

Najbardziej skutecznym rozwiązaniem może być napisać demona, który i śledzić na polu.tail -faccess.log$http_referer

Jednak szybkim i brudnym rozwiązaniem byłoby dodanie dodatkowego access_logpliku, rejestrowanie tylko $http_refererzmiennej za pomocą niestandardowej log_formati automatyczne obracanie dziennika co X minut.

  • Można tego dokonać za pomocą standardowych skryptów logrotate, które mogą wymagać płynnego ponownego uruchomienia nginx w celu ponownego otwarcia plików (np. Standardowa procedura, spójrz na / a / 15183322 na SO przez prosty czas- oparty na skrypcie)…

  • Lub, używając zmiennych wewnątrz access_log, być może poprzez wyciągnięcie najdrobniejszej specyfikacji $time_iso8601za pomocą dyrektywy maplub if(w zależności od tego, gdzie chcesz umieścić access_log).

Tak więc, z powyższym, możesz mieć 6 plików dziennika, z których każdy obejmuje okres 10 minut http_referer.Txx{0,1,2,3,4,5}x.log, np. Poprzez uzyskanie pierwszej cyfry minuty w celu odróżnienia każdego pliku.

Teraz wystarczy prosty skrypt powłoki, który mógłby uruchamiać się co 10 minut, catwszystkie powyższe pliki razem, potokować do sort, potokować do uniq -c, do sort -rn, do head -16i masz listę 16 najczęstszych Refererodmian - swobodnie decyduj, czy dowolne kombinacje liczb i pól przekraczają twoje kryteria, i dokonaj powiadomienia.

Następnie, po pojedynczym pomyślnym powiadomieniu, możesz usunąć wszystkie te 6 plików, aw kolejnych uruchomieniach nie wydawać żadnego powiadomienia, chyba że wszystkie sześć plików jest obecnych (i / lub pewna inna liczba, którą uważasz za stosowną).

cnst
źródło
To wygląda bardzo przydatne. Mogę prosić o zbyt wiele, ale jak w przypadku wcześniejszej odpowiedzi, czy miałbyś coś przeciwko pomocy ze skryptem?
Quintin Par
@QuintinPar That Brzmi nieprogramowo! ;-) Jeśli chcesz, jestem do wynajęcia i konsultacji; mój e-mail to [email protected], również na Constantine.SU
cnst
Całkowicie rozumiem. Wielkie dzięki za dotychczasową pomoc. Mam nadzieję, że pewnego dnia cię stać :-)
Quintin Par
1
@QuintinPar nie ma za co! Bez obaw, powinien to być dość prosty skrypt z powyższą specyfikacją; po prostu kwestia testowania, konfiguracji i pakowania. :)
cnst
1
Jesteś superbohaterem!
Quintin Par
13

Myślę, że lepiej byłoby to zrobić przy logtailu i grep. Nawet jeśli jest to możliwe w przypadku lua inline, nie chcesz tego narzutu dla każdego żądania, a szczególnie nie chcesz go, gdy jesteś Slashdotted.

Oto 5-sekundowa wersja. Umieść go w skrypcie i umieść wokół niego bardziej czytelny tekst, a będziesz złoty.

5 * * * * logtail -f /var/log/nginx/access_log -o /tmp/nginx-logtail.offset | grep -c "http://[^ ]slashdot.org"

Oczywiście całkowicie to ignoruje reddit.com i facebook.com oraz wszystkie miliony innych witryn, które mogą generować duży ruch. Nie wspominając o 100 różnych witrynach, z których każda odwiedza 20 osób. Prawdopodobnie powinieneś mieć zwykły stary próg ruchu, który spowoduje wysłanie wiadomości e-mail, niezależnie od strony polecającej.

Ladadadada
źródło
1
Problemem jest bycie proaktywnym. Muszę wiedzieć z dowolnej strony. Kolejne pytanie dotyczy tego, gdzie ustawić próg? Czy chodziło Ci o dodatkowe analizowanie dziennika? Również nie znalazłem –o w fourmilab.ch/webtools/logtail
Quintin Par
Próg zależy od tego, ile ruchu mogą obsłużyć twoje serwery. Tylko Ty możesz to ustawić. Jeśli chcesz szybciej otrzymywać powiadomienia, uruchom je co pięć minut zamiast co godzinę i podziel próg przez 12. -o Opcja dotyczy pliku przesunięcia, aby wiedział, od czego zacząć czytanie następnym razem.
Ladadadada
@Ladadadada, nie zgadzam się z tym, że narzut byłby znaczny, zobacz moje rozwiązanie - serverfault.com/a/870537/110020 - Uważam, że narzut byłby dość minimalny, gdyby został poprawnie zaimplementowany, szczególnie (1), jeśli twój backend jest naprawdę powolny, to narzut byłby znikomy, lub (2), jeśli twój backend jest już dość wąski i / lub odpowiednio buforowany, to powinieneś mieć małe problemy z obsługą ruchu w pierwszej kolejności i trochę dodatkowego obciążenia wygrał ” zrobić wgniecenie. Ogólnie rzecz biorąc, wydaje się, że to pytanie ma dwa przypadki użycia: (1), po prostu będąc poinformowanym, i (2) automatyczne skalowanie.
cnst
4

Dyrektywa nginx limit_req_zone może opierać swoje strefy na dowolnej zmiennej, w tym $ http_referrer.

http {
    limit_req_zone  $http_referrer  zone=one:10m   rate=1r/s;

    ...

    server {

        ...

        location /search/ {
            limit_req   zone=one  burst=5;
        }

Będziesz także chciał zrobić coś, aby ograniczyć wymagany stan na serwerze WWW, ponieważ nagłówki strony odsyłającej mogą być dość długie i zróżnicowane, a może pojawić się nieskończona odmiana. Za pomocą funkcji split_clients nginx można ustawić zmienną dla wszystkich żądań opartą na haszu nagłówka strony odsyłającej. Poniższy przykład wykorzystuje tylko 10 bucków, ale równie łatwo możesz to zrobić z 1000. Więc jeśli zostaniesz slashdotowany, osoby, które skierowały się do hashowania do tego samego segmentu, co adres URL slashdot, również zostaną zablokowane, ale możesz ograniczyć to do 0,1% odwiedzających, używając 1000 segmentów w split_clients.

Wyglądałoby to mniej więcej tak (całkowicie nie przetestowane, ale poprawne kierunkowo):

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;
    }
rmalayter
źródło
To interesujące podejście; jednak uważam, że pytanie dotyczy automatycznego alarmu, gdy ma miejsce efekt Slashdot; wydaje się, że Twoje rozwiązanie rozwiązuje problem losowego blokowania około 10% użytkowników. Co więcej, uważam, że twoje uzasadnienie użycia split_clientsmoże być błędne - limit_reqopiera się na „nieszczelnym pojemniku”, co oznacza, że ​​ogólny stan nigdy nie powinien przekraczać wielkości określonej strefy.
cnst
2

Tak, oczywiście jest to możliwe w NGINX!

Co możesz zrobić, to wdrożyć następujący DFA :

  1. Zaimplementuj ograniczenie szybkości, oparte na $http_refererewentualnym użyciu wyrażeń regularnych poprzez a w mapcelu normalizacji wartości. Po przekroczeniu limitu pojawia się strona błędu wewnętrznego, którą można przechwycić przez error_pagemoduł obsługi zgodnie z powiązanym pytaniem , przechodząc do nowej lokalizacji wewnętrznej jako przekierowanie wewnętrzne (niewidoczne dla klienta).

  2. W powyższej lokalizacji dla przekroczonych limitów wykonujesz żądanie alertu, pozwalając zewnętrznej logice wykonać powiadomienie; to żądanie jest następnie buforowane, zapewniając, że otrzymasz tylko 1 unikalne żądanie w danym oknie czasowym.

  3. Złap kod stanu HTTP z poprzedniego żądania (zwracając kod stanu ≥ 300 i używając proxy_intercept_errors onlub, alternatywnie, skorzystaj z domyślnie niezbudowanego auth_requestlub add_after_bodyzrób „bezpłatne” żądanie) i wypełnij pierwotne żądanie, jakby poprzedni krok nie był zaangażowany. Pamiętaj, że aby error_pageto zadziałało , musimy włączyć obsługę rekurencyjną .

Oto mój PoC i MVP, również na 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;
}

Pamiętaj, że działa to zgodnie z oczekiwaniami:

% 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"
%

Widać, że pierwsze żądanie skutkuje jednym trafieniem frontonu i jednego backendu, zgodnie z oczekiwaniami (musiałem dodać fikcyjny backend do lokalizacji, która ma limit_req, ponieważ ponieważ return 200miałby pierwszeństwo przed limitami, prawdziwy backend nie jest konieczny do końca obsługi).

Drugie żądanie jest powyżej limitu, więc wysyłamy alert (otrzymujemy 200) i buforujemy go, zwracając 429(jest to konieczne ze względu na wspomniane ograniczenie, że żądania poniżej 300 nie mogą zostać przechwycone), które są następnie przechwytywane przez interfejs , który jest teraz wolny i może robić, co chce.

Trzecie żądanie wciąż przekracza limit, ale już wysłaliśmy alert, więc żaden nowy alert nie zostanie wysłany.

Gotowy! Nie zapomnij rozwidlić go na GitHub!

cnst
źródło
Czy dwa warunki ograniczające prędkość mogą ze sobą współpracować? Korzystam z tego w tej chwili: serverfault.com/a/869793/26763
Quintin Par
@QuintinPar :-) Myślę, że będzie to zależeć od tego, jak go użyjesz - oczywistym problemem byłoby rozróżnienie w jednym miejscu, którego limit wprowadził warunek; ale jeśli ten jest a limit_req, a drugi to limit_conn, po prostu użyj limit_req_status 429powyższego (wymaga bardzo nowego nginx) i myślę, że powinieneś być złoty; mogą istnieć inne opcje (jedną z nich na pewno jest połączenie nginx w / set_real_ip_from, ale w zależności od tego, co dokładnie chcesz zrobić, mogą być bardziej wydajne opcje).
cnst
@QuintinPar, jeśli czegoś brakuje w mojej odpowiedzi, daj mi znać. BTW, pamiętaj, że po osiągnięciu limitu i wywołaniu skryptu, dopóki skrypt nie zostanie poprawnie buforowany przez nginx, zawartość może zostać opóźniona; np. możesz chcieć zaimplementować skrypt asynchronicznie z czymś podobnym golanglub zajrzeć do opcji limitu czasu dla upstreams; także może chcieć również użyć proxy_cache_lock oni ewentualnie dodać obsługę błędów, co zrobić, jeśli skrypt zawiedzie (np. przy użyciu, error_pagea także proxy_intercept_errorsponownie). Ufam, że mój POC to dobry początek. :)
cnst
Dziękujemy za próbę tego. Jednym z głównych problemów nadal jest to, że używam limit_req i limit_conn już na poziomie http i dotyczy to wszystkich stron internetowych, które mam. Nie mogę tego zmienić. Dlatego w tym rozwiązaniu jest używana funkcja przeznaczona do czegoś innego. Jakieś inne podejście do tego rozwiązania?
Quintin Par
@QuintinPar Co z zagnieżdżeniem instancji Nginx, z których każda będzie używać jednego zestawu limit_req/ limit_conn? Na przykład po prostu umieść powyższą konfigurację przed bieżącym serwerem frontonu. Możesz użyć set_real_ip_fromw nginx w górę, aby upewnić się, że adresy IP są poprawnie rozliczane wzdłuż linii. W przeciwnym razie, jeśli nadal nie będzie pasować, myślę, że musisz precyzyjniej określić swoje ograniczenia i specyfikację - o jakich natężeniach ruchu mówimy? Jak często statystyki muszą się uruchamiać (1min / 5min / 1h)? Co jest nie tak ze starym logtailrozwiązaniem?
cnst