![](https://habrastorage.org/getpro/habr/post_images/dec/a7d/146/deca7d146accb628876bb6d464bf66d1.png)
この投稿は、 nginxでのluaの使用の続きです 。
メモリでのキャッシュについて説明しました。ここでは、luaを使用して、着信リクエストをnginx-balancerの一種のファイアウォールとしてフィルタリングします。 2GISには似たようなものがありました 。 独自の自転車を持っています:)ダイナミクスとスタティックを共有するために、NATとホワイトリストを考慮に入れます。 そして、もちろん、既製のモジュールを使用すると機能しない特定のロジックをいつでも入手できます。
このスキームは静かで楽になり(実際には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スクリプトを通過します。
同時に、リクエストの履歴を保存するための2つのテーブルreq_limitとban_listと、それに応じて既に禁止されているリストがあります(詳細は以下)。
また、自転車の代わりにIP経由でホワイトリストを実装するために、geo nginxモジュールが使用され、lua_req_whitelist変数の値が次のように使用されました。
if ngx.var.lua_req_whitelist ~= '1' then -- IP , end
静的/動的(ディスク/バックエンドサーバー上のファイルの要求)を確認するには、要求されたファイルの名前を簡単に確認します(ここで、ビジネスロジックに適応することで実装を複雑にできます)。
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も考慮され、特別なCookieが追加されます。 3つの要素全体がユーザーIDを構成します。 悪役が送信されたCookieを無視してサーバーを空洞化した場合、最悪の場合、そのIP /サブネットは禁止されます。 同時に、すでにCookieを受け取っているこのサブネットのユーザーは、静かにさらに動作します(IPの禁止の場合を除く)。 ソリューションは完璧ではありませんが、国/携帯電話会社の半分を1人のユーザーとして数えるよりも優れています。
Cookieの生成と検証:
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つのバージョンをチェックします:静的/動的、1つのパス/異なる。 直接チェックは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 /アナログに固定できます。 これはすでに話題外です。
もちろん、これはすべて単なる例であり、銀のコピーペーストではありません。 さらに、指定された数の制限が天井から示されます。
ヘッダーの画像はここから取得されます 。