基于 Nginx 与 Lua 脚本自建高防 CDN 节点的底层逻辑与代码实践
摘要:# 自己动手,给网站穿上“铁布衫”:Nginx+Lua自建高防节点那些事儿 ˃ 你信不信,很多中小站长花大价钱买来的“高防CDN”,核心原理可能就藏在你手边这两样免费工具里。 那天晚上,一个做游戏私服的朋友给我打电话,声音都带着颤:“又被打了,流量跑满…
自己动手,给网站穿上“铁布衫”:Nginx+Lua自建高防节点那些事儿
你信不信,很多中小站长花大价钱买来的“高防CDN”,核心原理可能就藏在你手边这两样免费工具里。
那天晚上,一个做游戏私服的朋友给我打电话,声音都带着颤:“又被打了,流量跑满,机房直接给我拔线了。”他之前用的某家“高防”,月付好几千,宣传页上写着“T级防护、智能清洗”,结果攻击一来,连带着“高防”一起躺平,页面直接显示“正在清洗中,请稍候”。说白了,就是被扔进了一个“黑洞”,真用户也进不来。
我当时就跟他说:“你要不嫌麻烦,自己搭个‘乞丐版’高防节点试试?成本可能就一台VPS的钱,但有些场景下,比那些花架子管用。”他后来真搞了,用Nginx和Lua脚本搭了个简单的防护层,放在业务服务器前面。效果嘛,用他的话说:“对付那些拿公开工具瞎扫的‘小学生’攻击,以及低配版的CC,简直像开了‘金钟罩’。当然,真遇上大佬的‘神装’DDoS,该跑还得跑,但至少把大部分‘杂鱼’过滤掉了。”
今天,我就把这套自己折腾、也帮朋友搞过好几次的“土法炼钢”式高防节点思路,掰开揉碎了跟你聊聊。我们不谈那些云山雾罩的“智能”、“全球调度”,就聊聊怎么用代码,实实在在地给你的网站多穿一件“防弹衣”。
一、为什么是Nginx+Lua?先泼盆冷水
先说大实话:别指望用这套方案去硬扛动辄几百G的流量型DDoS。那种攻击,拼的是带宽和硬件,是“钞能力”的战场。你自建的节点,出口带宽和硬件资源是硬伤。
那它的用武之地在哪?
- 精准防护CC攻击:CC(Challenge Collapsar)攻击,说白了就是用大量傀儡机模拟正常用户,疯狂请求你的动态页面(比如登录接口、搜索接口)。这种攻击不耗你太多带宽,但能瞬间吃光你服务器的CPU和数据库连接。Nginx+Lua的强项,就是能在请求到达你源站之前,用灵活的规则把它们识别出来、干掉。
- 隐藏源站,避免被“点对点”打穿:很多攻击者会先探测你的真实服务器IP。你把自建的高防节点作为对外公开的IP,真实服务器IP藏起来。攻击者打的是你的节点,节点扛不住了,你可以快速切换另一个节点IP,而你的源站稳如泰山。
- 低成本定制化规则:商业高防的规则往往是通用的,或者定制起来非常昂贵。你自己写的Lua脚本,想怎么玩就怎么玩——可以针对某个可疑的URL参数、某个特定的User-Agent、甚至每秒请求超过10次就弹出一个验证码。灵活性是最大的优势。
说白了,这套方案的核心思想是:用极低的成本,建立一道灵活的“智能过滤网”,把大部分“非专业”的、规则化的攻击挡在门外,让真正的攻击成本变高。 它是“护甲”,不是“无敌盾”。
二、核心三板斧:流量怎么被“拿捏”?
自建高防节点,逻辑上可以抽象成三层过滤,像漏斗一样一层层筛掉恶意流量。
第一层:基础筛子(Nginx层面搞定)
这层主要靠Nginx自身的配置,几乎不消耗额外性能。
- 限制连接频率与速率:这是最基础的。在Nginx的
http或server模块里,你可以用limit_conn_zone和limit_req_zone来限制单个IP的并发连接数和请求速率。比如,一个IP每秒最多请求20次动态接口,超过的直接返回503。这能防住最原始的CC攻击。 - 屏蔽“坏IP”:维护一个IP黑名单。你可以用
deny指令直接写在配置里,但更灵活的做法是,把这个名单放在一个外部文件或者内存数据库中(比如Redis),用Lua去读取。一些常见的攻击IP段、数据中心IP,可以直接丢进去。
# 示例:在http块中定义限制区域
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;
server {
location /api/ {
limit_req zone=one burst=5 nodelay;
# 超过频率的请求,这里就直接被拦截了
proxy_pass http://your_real_server;
}
}
}
第二层:灵魂过滤(Lua脚本上场)
这一层是精髓,也是自建方案比纯Nginx配置强大的地方。你需要安装 ngx_http_lua_module 模块。
- 请求特征分析:Lua脚本可以拿到完整的请求信息。比如,攻击者常用的扫描器、漏洞利用工具,它们的User-Agent、HTTP头字段往往有固定特征。你可以写规则去匹配和拦截。
- 复杂频率统计:Nginx自带的频率限制是基于IP的,但攻击者可能用代理IP池。Lua可以帮你实现更复杂的统计,比如:针对某个用户ID(从Cookie或参数中提取)进行频率限制;或者统计某个URI的全局访问频率,异常飙升时自动启用更严格的验证。
- 人机验证挑战:对于可疑但又不能直接封的请求,可以弹出一个简单的JS挑战或验证码。比如,在Lua里返回一段JavaScript代码,要求客户端计算一个简单的结果并回传,真正的浏览器能轻松执行,而很多简单的攻击脚本就“懵”了。这招防CC非常有效。
-- 示例:一个简单的Lua脚本,检查请求中是否包含可疑的SQL注入特征(伪代码)
local request_uri = ngx.var.request_uri
local args = ngx.req.get_uri_args()
-- 一个非常简单的关键词过滤(实际应用需要更复杂的正则和混淆处理)
local sql_keywords = {"union select", "' or '1'='1", "drop table", "--"}
for _, kw in ipairs(sql_keywords) do
if string.find(request_uri:lower(), kw) or (args and check_args_contain(args, kw)) then
ngx.log(ngx.WARN, "SQLi attempt blocked from IP: ", ngx.var.remote_addr)
ngx.exit(403) -- 直接拒绝
end
end
-- 如果通过检查,继续转发
第三层:状态共享与联动(用上Redis)
单台Nginx节点的内存是有限的,统计信息无法在多台节点间共享。这时候就需要引入Redis。
- 集群频率统计:把IP、用户ID等维度的访问计数存到Redis里,设置过期时间。这样,即使你有多个自建高防节点分布在不同的地方,它们也能共享攻击状态,协同防护。一个IP在节点A上被识别为攻击者,下一秒访问节点B,照样能被识别并拦截。
- 动态黑名单:Lua脚本可以将确认的攻击者IP实时写入Redis黑名单,其他所有节点都从这个公共黑名单读取。实现攻击情报的“秒级同步”。
-- 示例:使用Redis进行集群频率限制(需要安装lua-resty-redis库)
local redis = require "resty.redis"
local red = redis:new()
red:connect("your_redis_ip", 6379)
local key = "req_limit:" .. ngx.var.remote_addr .. ":" .. ngx.var.uri
local current = red:incr(key) -- 计数加1
if current == 1 then
red:expire(key, 60) -- 如果是第一次,设置60秒过期
end
if current > 50 then -- 如果60秒内超过50次请求
red:sadd("global_blacklist", ngx.var.remote_addr) -- 加入全局黑名单
ngx.exit(503)
end
三、动手实践:一个最小可用的“高防”配置
理论说再多,不如来点实在的。假设你已经装好了带Lua模块的OpenResty(这是Nginx+Lua的“全家桶”版本,省事)。
目标:对 /login 这个登录接口,实施“IP+用户账号”双维度频率限制,并给可疑请求弹出JS挑战。
-
Nginx主配置 (
nginx.conf):http { lua_package_path "/your/path/to/lua-scripts/?.lua;;"; lua_shared_dict my_limit 10m; # 共享内存,用于单节点内快速计数 upstream real_backend { server 你的真实源站IP:端口; } server { listen 80; server_name your.domain.com; location /login { access_by_lua_file /your/path/to/lua-scripts/login_protect.lua; proxy_pass http://real_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 其他location... } } -
Lua防护脚本 (
login_protect.lua):local ngx = ngx local http = require "resty.http" local cjson = require "cjson" -- 1. 获取客户端IP和请求参数 local client_ip = ngx.var.remote_addr local args = ngx.req.get_post_args() or {} local username = args.username -- 2. 连接Redis(假设Redis存储集群统计信息) local redis = require "resty.redis" local red = redis:new() local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "Failed to connect to Redis: ", err) -- 连接失败,可以选择直接放行或拒绝,这里选择放行但记录日志 -- return end -- 3. 检查IP是否在全局黑名单 local is_banned, err = red:sismember("global:blacklist:ip", client_ip) if is_banned == 1 then ngx.exit(444) -- 直接关闭连接,不响应 end -- 4. 双维度频率检查:IP维度 和 (IP+用户名)维度 local ip_key = "limit:login:ip:" .. client_ip local user_key = "limit:login:user:" .. client_ip .. ":" .. (username or "unknown") local ip_count = red:incr(ip_key) local user_count = red:incr(user_key) if ip_count == 1 then red:expire(ip_key, 60) end -- 60秒内计数 if user_count == 1 then red:expire(user_key, 300) end -- 用户名维度5分钟 -- 5. 判断逻辑 if (ip_count > 30) or (username and user_count > 5) then -- 触发JS挑战 local challenge_html = [[ <html><body> <script> function calc() { return 7 + 3; } // 一个极其简单的计算 fetch(window.location.href, { method: 'POST', headers: {'X-Challenge-Result': calc()}, body: new FormData(document.getElementById('form')) }).then(r => r.text()).then(d => document.body.innerHTML = d); </script> <form id="form"><input type="hidden" name="original_request" value="]] .. ngx.var.request_uri .. [["></form> </body></html> ]] ngx.header.content_type = "text/html" ngx.say(challenge_html) ngx.exit(200) end -- 6. 如果是携带了挑战结果的请求(简化处理) local challenge_result = ngx.req.get_headers()["X-Challenge-Result"] if challenge_result and challenge_result == "10" then -- 挑战通过,清理计数,放行到源站 red:del(ip_key) red:del(user_key) return end -- 7. 正常请求,且频率未超标,直接放行 -- Lua脚本执行完毕,Nginx会继续执行 proxy_pass 转发到源站(注意:这是一个极度简化的示例,用于说明逻辑。生产环境需要处理各种边界条件、错误、以及更安全的挑战机制。)
四、最后的大实话:优势和天花板
优势:
- 成本极低:一台配置还行的VPS就能起步,软件全部开源。
- 规则随心:你的业务你最懂,可以写出最适合自己业务的防护规则。
- 学习价值:亲手搭建一遍,你对HTTP协议、攻击原理、防护逻辑的理解会深好几个层次。以后再去用商业产品,你一眼就能看出它大概在哪个层面起作用。
天花板与风险:
- 带宽是爹:流量型DDoS一来,VPS那点带宽瞬间清零,机房照样拔你线。
- 维护成本:规则要持续更新,脚本可能有BUG,需要你花时间盯着。
- 单点故障:如果你只有一个自建节点,它挂了,业务全挂。所以,至少搞两个节点,用DNS做简单轮询或故障切换,是必须的。
所以,我的建议是:把它当成一个“加强版WAF”或“CC防火墙”来用,而不是一个完整的“高防CDN”。 对于预算有限、又需要一定定制化防护的中小业务,这绝对是一把利器。它可以作为你整体安全架构中的一环,与云服务商的“流量清洗”服务搭配使用——自建节点先做精细过滤,过滤不掉的大流量洪水,再交给“钞能力”去扛。
行了,代码和思路都在这儿了。要不要动手试试,给你裸奔的源站,穿上一件自己打的“铁布衫”?

