Lua - мощный и простой язык на основе C++. Интегрируя Lua с Nginx мы получаем гибкий инструмент для решения разнообразных задач. Так, на Хабре появилась статья, где описывается, как с помощью Lua отражать ддос-атаки. Как собрать Nginx с Lua можно посмотреть тут. -------------------------------------------------------------- Данная схема сейчас спокойно и ненапряжно (практически не сказывается на использовании cpu) обрабатывает порядка 1200 запросов/сек. На предельные величины не тестировалось. Пожалуй, к счастью Хочется обрабатывать все входящие запросы сразу по поступлению, а не по факту строчки в access_log (который еще небось и выключен для той же статики). Не вопрос, вешаем обработчик глобально на весь http: Код: http { include lua/req.conf; } # содержимое lua/req.conf # память под хранение счетчиков запросов (надо много, хотя вытеснение старых записей по LRU допустимо) lua_shared_dict req_limit 1024m; # память под хранение списка забаненных (список должен быть небольшой, но вытеснение крайне нежелательно) lua_shared_dict ban_list 128m; # белый список. проверки не выполняются, защитная кука не ставится geo $lua_req_whitelist { default 0; 12.34.56.78/24 1; } # настройка init_by_lua ' -- секретная соль для защитной куки lua_req_priv_key = "secretpassphrase" -- имя защитной куки lua_req_cookie_name = "reqcookiename" -- путь до файла лога забаненных lua_req_ban_log = "/path/to/log/file" -- допустимые лимиты на запросы (в мин) -- числа исключительно для примера lua_req_d_one = 42 -- динамика на один URI lua_req_d_mul = 84 -- динамика на разные URI lua_req_s_one = 100 -- статика на один URI lua_req_s_mul = 200 -- статика на разные URI lua_req_d_ip = 200 -- динамика с одного IP lua_req_s_ip = 400 -- статика с одного IP -- бан на 10 минут lua_req_ban_ttl = 600 -- служебное math.randomseed(math.floor(ngx.now()*1000)) '; # подключение основного скрипта, встраивающегося в access стадию обработки запросов access_by_lua_file /path/to/nginx/lua/req.lua; Теперь все запросы, приходящие в nginx, пройдут через наш скрипт req.lua. При этом у нас есть две таблицы req_limit и ban_list для хранения истории запросов и списка уже забаненных соотвественно (подробнее ниже). А для реализации whitelist по IP вместо велосипедов использован модуль geo nginx, проставляющий значение переменной lua_req_whitelist, которая используется примерно так: Код: if ngx.var.lua_req_whitelist ~= '1' then -- IP не из белого списка, выполняем проверки end Для проверки статика/динамика (запрос за файлом на диске/backend серверу) делаем простую проверку по имени запрашиваемого файла (тут можно усложнять реализацию, подстраиваясь под свою бизнес логику): Код: function string.endswith(haystack, needle) return (needle == '') or (needle == string.sub(haystack, -string.len(needle))) end local function path_is_static(path) local exts = {'js', 'css', 'png', 'jpg', 'jpeg', 'gif', 'xml', 'ico', 'swf'} path = path:lower() for _,ext in ipairs(exts) do if path:endswith(ext) then return true end end return false end local uri_path = ngx.var.request_uri if ngx.var.is_args == '?' then uri_path = uri_path:gsub('^([^?]+)\\?.*$', '%1') end local is_static = path_is_static(uri_path) Для хоть какой-то обработки NAT, кроме IP клиентов так же учитывается их UserAgent и проставляется спец кука. Все три элемента в целом и составляют идентификатор пользователя. Если некий злодей долбит сервер, игнорируя передаваемую куку, то в худшем случае просто будет забанен его IP/подсеть. При этом те пользователи с этой подсети, кто уже получил ранее куку, будут спокойно работать дальше (кроме случая бана по IP). Решение не идеальное, но все же лучше, чем считать полстраны/мобильного оператора за одного пользователя. Генерация и проверки куки: Код: local function gen_cookie_rand() return tostring(math.random(2147483647)) end local function gen_cookie(prefix, rnd) return ngx.encode_base64( -- для разделения двух клиентов с одного IP и с одинаковыми UserAgent, вмешиваем каждому случайное число ngx.sha1_bin(ngx.today() .. prefix .. lua_req_priv_key .. rnd) ) end local uri = ngx.var.request_uri -- запрашиваемый URI local host = ngx.var.http_host -- к какому домену пришел запрос (если у вас nginx обрабатывает несколько доменов) local ip = ngx.var.remote_addr local user_agent = ngx.var.http_user_agent or '' if user_agent:len() > 0 then user_agent = ngx.encode_base64(ngx.sha1_bin(user_agent)) end local key_prefix = ip .. ':' .. user_agent -- проверка контрольной куки local user_cookie = ngx.unescape_uri(ngx.var['cookie_' .. lua_req_cookie_name]) or '' local rnd = gen_cookie_rand() local p = user_cookie:find('_') if p then rnd = user_cookie:sub(p+1) user_cookie = user_cookie:sub(1, p-1) end local control_cookie = gen_cookie(key_prefix, rnd) if user_cookie ~= control_cookie then user_cookie = '' rnd = gen_cookie_rand() control_cookie = gen_cookie(key_prefix, rnd) end key_prefix = key_prefix .. ':' .. user_cookie ngx.header['Set-Cookie'] = string.format('%s=%s; path=/; expires=%s', lua_req_cookie_name, ngx.escape_uri(control_cookie .. '_' .. rnd), ngx.cookie_time(ngx.time()+24*3600) ) Теперь в key_prefix содержится идентификатор клиента, чей запрос мы обрабатываем. Если данный клиент уже забанен, то дальнейшая обработка не нужна: Код: local ban_key = key_prefix..':ban' if ban_list:get(ban_key) or ban_list:get(ip..':ban') then -- проверка ключа и проверка бана вообще в целом по IP return ngx.exit(ngx.HTTP_FORBIDDEN) end Ключ получили, бан проверили, теперь можно посчитать, не превышает ли данный запрос какой из лимитов: Код: -- проверка обоих вариантов: на один URI и на разные URI local limits = { [false] = { [false] = lua_req_d_mul, -- динамика на разные URI [true] = lua_req_d_one, -- динамика на один URI }, [true] = { [false] = lua_req_s_mul, -- статика на разные URI [true] = lua_req_s_one, -- статика на один URI } } for _,one_path in ipairs({true, false}) do local limit = limits[is_static][one_path] local key = {key_prefix} -- разделение статики и динамики в имени ключа if is_static then table.insert(key, 'S') else table.insert(key, 'D') end -- для проверки запросов к одному и тому же пути (для всяких API может не подойти) if one_path then table.insert(key, host..uri) end -- получаем ключ вида "12.34.56.78:useragentsha1base64:cookiesha1base64:S:site.com/path/to/file" key = table.concat(key, ':') local exhaust = check_limit_exhaust(key, limit, ban_ttl) if exhaust then return ngx.exit(ngx.HTTP_FORBIDDEN) end end Проверяем 4 варианта счетчиков: статика/динамика, по одному пути/по разным. Непосредственные проверки выполняются в check_limit_exhaust(): Код: local function check_limit_exhaust(key, limit, cnt_ttl) local key_ts = key..':ts' local cnt, _ = req_limit:incr(key, 1) -- если ключа нет, то это первый запрос -- добавляем счетчик и отметку с текущим временем if cnt == nil then if req_limit:add(key, 1, cnt_ttl) then req_limit:set(key_ts, ngx.now(), cnt_ttl) end return false end -- если не превысили лимит (пока даже без учета интервалов) if cnt <= limit then return false end -- если есть превышение лимита (без учета интервалов), -- то нужно получить последнюю отметку интервала и проверить лимит уже с учетом интервала local key_lock = key..':lock' local key_lock_ttl = 0.5 local ts local try_until = ngx.now() + key_lock_ttl local locked while true do locked = req_limit:add(key_lock, 1, key_lock_ttl) cnt = req_limit:get(key) ts = req_limit:get(key_ts) if locked or (try_until < ngx.now()) then break end ngx.sleep(0.01) end -- если не удалось получить актуальные данные и получить лок на обновление - крики, паника, запрещаем запрос. -- при этом не добавляем данный IP в blacklist -- у вас может быть иная логика if (not locked) and ((not cnt) or (not ts)) then return true, 'lock_failed' end -- за сколько времени (в сек) накоплен счетчик local ts_diff = math.max(0.001, ngx.now() - ts) -- нормализация счетчика на секундный интервал local cnt_norm = math.floor(cnt / ts_diff) -- если нормализованное количество запросов не превысило лимит if cnt_norm <= limit then -- корректировка ts и cnt (если что в этих set'ах поломается - просто потом еще раз попадем в эту ветку) req_limit:set(key, cnt_norm, cnt_ttl) req_limit:set(key_ts, ngx.now() - 1, cnt_ttl) -- лок снимаем; в blacklist не добавляем; запрос не блокируем if locked then req_limit:delete(key_lock) end return false end -- превысили лимит. баним, запрос блокируем, пишем в лог req_limit:delete(key) req_limit:delete(key_ts) if locked then req_limit:delete(key_lock) end return true, cnt_norm end Кроме непосредственного бана на lua_req_ban_ttl секунд, можно реализовать постоянное хранение, а заодно прикрутить логгирование и проброс забаненных по IP в iptables/аналоги. Это уже вне темы поста. Все это, само собой, лишь пример, а не серебряная пуля-копипаста. Тем более приведенные числа лимитов указаны с потолка. Источник: http://habrahabr.ru/post/215235/
It is a pity, that I can not participate in discussion now. It is not enough information. But with pleasure I will watch this theme.