# 组件分享
# ipset_block组件
# 组件说明
在CC攻击场景下,当攻击请求的QPS超过了节点的处理QPS,这个时候WAF拦截CC攻击请求的同时,也会因为负载过高而无法正常处理业务请求。
所以需要在网络层直接对持续攻击的IP进行封禁,避免应用层负载过高的情况。组件会负责将需要封禁的IP通过HTTP请求发送到节点的jxwaf_ipset_block程序。
jxwaf_ipset_block会将接受到的攻击IP,调用本地的ipset进行封禁。
# 代码分析
-- 初始化模块
local _M = {}
_M.version = "jxwaf4"
-- 引入依赖
local http = require "resty.jxwaf.http" -- 用于创建HTTP请求的自定义模块
local cjson = require "cjson.safe" -- 安全的JSON编解码模块
local unify_action = require "resty.jxwaf.unify_action" -- 统一行动处理的自定义模块
-- 功能:发送IP封禁请求到远端服务器
local function send_ipset_block(period,ip_ban_server,black_ip,ip_ban_auth)
-- 构造IP封禁服务端点的URL
local ip_ban_website = "http://"..ip_ban_server.."/banip"
local httpc = http.new()
httpc:set_timeouts(10000, 10000, 10000) -- 设置HTTP超时
-- 准备请求主体
local body_data = {
network_block_ip = black_ip,
auth = ip_ban_auth
}
-- 发送POST请求
local res, err = httpc:request_uri( ip_ban_website , {
method = "POST",
headers = {
["Content-Type"] = "application/json;charset=UTF-8",
},
body = cjson.encode(body_data)
})
-- 错误处理
if not res then
ngx.log(ngx.ERR,"send http failed to request: ", err)
return
end
if res.status ~= 200 then
ngx.log(ngx.ERR, "IP ban request returned non-200 response: ", res.status)
return
end
-- 解析响应体
local res_body = cjson.decode(res.body)
if not res_body then
ngx.log(ngx.ERR,"send fail,failed to decode resp body")
ngx.log(ngx.ERR,res.body)
return
end
-- 检查结果
if res_body['result'] ~= true then
ngx.log(ngx.ERR,"result ~= true " )
ngx.log(ngx.ERR,res.body)
return
end
return true
end
-- 功能:检查请求是否符合封禁条件
function _M.check(conf_data)
-- 确保配置数据存在
if conf_data == nil then
return
end
-- 验证和提取必要配置
local jxwaf_ipset_node = conf_data['jxwaf_ipset_node']
if type(jxwaf_ipset_node) ~= 'table' then
return
end
local auth = conf_data['auth']
if type(auth) ~= 'string' then
return
end
local flow_ip_region_block = conf_data['flow_ip_region_block']
if type(flow_ip_region_block) ~= 'boolean' then
ngx.log(ngx.ERR,type(flow_ip_region_block))
return
end
local flow_rule_protection = conf_data['flow_rule_protection']
if type(flow_rule_protection) ~= 'table' then
return
end
local flow_engine_protection = conf_data['flow_engine_protection']
if type(flow_engine_protection) ~= 'table' then
return
end
-- 获取请求处理结果
local flow_ip_region_block_result = ngx.ctx.flow_ip_region_block_result
local flow_rule_protection_result = ngx.ctx.flow_rule_protection_result
local flow_engine_protection_result = ngx.ctx.flow_engine_protection_result
-- 检查是否触发封禁条件
local check_result
if flow_ip_region_block == true and flow_ip_region_block_result then
check_result = true
end
for _,v in ipairs(flow_rule_protection) do
if flow_rule_protection_result[v] then
check_result = true
end
end
for _,v in ipairs(flow_engine_protection) do
if flow_engine_protection_result[v] then
check_result = true
end
end
-- 如果触发封禁条件
if check_result then
local src_ip = ngx.ctx.src_ip or ngx.var.remote_addr -- 获取来源IP
-- 对每个IP设置发送封禁请求
for _,v in ipairs(jxwaf_ipset_node) do
-- ngx.timer.at的调用,第一个参数为0表示立即执行,第二个参数是要调用的函数及其参数
local ok, err = ngx.timer.at(0, send_ipset_block, v, src_ip, auth)
if not ok then
-- 如果创建定时器失败,记录错误(除非进程正在退出)
if err ~= "process exiting" then
ngx.log(ngx.ERR, "failed to create the send send_ipset_block http timer: ", err)
end
end
end
-- 更新和记录行为日志
local waf_log = ngx.ctx.waf_log
waf_log['waf_action'] = "ipset_block" -- 行为标记为IP封禁
ngx.ctx.waf_log = waf_log
-- 调用统一行动处理模块执行拒绝响应
unify_action.reject_response()
-- 返回true表示请求被拦截并处理
return true
end
-- 如果没有触发封禁条件,则不执行任何操作
return
end
-- 将模块返回,使其功能可被外部调用
return _M
# 组件部署说明
JXWAF控制台 -> 防护管理 -> 分析组件 -> 新建组件
# 组件名称
ipset_block
# 组件描述
ipset封禁组件
# CODE
bG9jYWwgX00gPSB7fQpfTS52ZXJzaW9uID0gImp4d2FmNCIKbG9jYWwgaHR0cCA9IHJlcXVpcmUgInJlc3R5Lmp4d2FmLmh0dHAiCmxvY2FsIGNqc29uID0gcmVxdWlyZSAiY2pzb24uc2FmZSIKbG9jYWwgdW5pZnlfYWN0aW9uID0gcmVxdWlyZSAicmVzdHkuanh3YWYudW5pZnlfYWN0aW9uIgoKbG9jYWwgZnVuY3Rpb24gc2VuZF9pcHNldF9ibG9jayhwZXJpb2QsaXBfYmFuX3NlcnZlcixibGFja19pcCxpcF9iYW5fYXV0aCkKICBsb2NhbCBpcF9iYW5fd2Vic2l0ZSA9ICJodHRwOi8vIi4uaXBfYmFuX3NlcnZlci4uIi9iYW5pcCIKICBsb2NhbCBodHRwYyA9IGh0dHAubmV3KCkKICBodHRwYzpzZXRfdGltZW91dHMoMTAwMDAsIDEwMDAwLCAxMDAwMCkKICBsb2NhbCBib2R5X2RhdGEgPSB7CiAgICBuZXR3b3JrX2Jsb2NrX2lwID0gIGJsYWNrX2lwLAogICAgYXV0aCA9IGlwX2Jhbl9hdXRoCiAgfQogIGxvY2FsIHJlcywgZXJyID0gaHR0cGM6cmVxdWVzdF91cmkoIGlwX2Jhbl93ZWJzaXRlICwgewogICAgbWV0aG9kID0gIlBPU1QiLAogICAgaGVhZGVycyA9IHsKICAgICAgWyJDb250ZW50LVR5cGUiXSA9ICJhcHBsaWNhdGlvbi9qc29uO2NoYXJzZXQ9VVRGLTgiLAogICAgfSwKICAgIGJvZHkgPSBjanNvbi5lbmNvZGUoYm9keV9kYXRhKQogIH0pCiAgaWYgbm90IHJlcyB0aGVuCiAgICBuZ3gubG9nKG5neC5FUlIsInNlbmQgaHR0cCBmYWlsZWQgdG8gcmVxdWVzdDogIiwgZXJyKQogICAgcmV0dXJuIAogIGVuZAogIGlmIHJlcy5zdGF0dXMgfj0gMjAwIHRoZW4KICAgIG5neC5sb2cobmd4LkVSUiwgIklQIGJhbiByZXF1ZXN0IHJldHVybmVkIG5vbi0yMDAgcmVzcG9uc2U6ICIsIHJlcy5zdGF0dXMpCiAgICByZXR1cm4KICBlbmQKICBsb2NhbCByZXNfYm9keSA9IGNqc29uLmRlY29kZShyZXMuYm9keSkKICBpZiBub3QgcmVzX2JvZHkgdGhlbgogICAgbmd4LmxvZyhuZ3guRVJSLCJzZW5kICBmYWlsLGZhaWxlZCB0byBkZWNvZGUgcmVzcCBib2R5IikKICAgIG5neC5sb2cobmd4LkVSUixyZXMuYm9keSkKICAgIHJldHVybgogIGVuZAogIGlmIHJlc19ib2R5WydyZXN1bHQnXSB+PSB0cnVlIHRoZW4KICAgIG5neC5sb2cobmd4LkVSUiwicmVzdWx0IH49IHRydWUgIiApCiAgICBuZ3gubG9nKG5neC5FUlIscmVzLmJvZHkpCiAgICByZXR1cm4KICBlbmQKICByZXR1cm4gdHJ1ZQplbmQKCmZ1bmN0aW9uIF9NLmNoZWNrKGNvbmZfZGF0YSkKICBpZiBjb25mX2RhdGEgPT0gbmlsIHRoZW4KICAgIHJldHVybiAKICBlbmQKCiAgbG9jYWwganh3YWZfaXBzZXRfbm9kZSA9ICBjb25mX2RhdGFbJ2p4d2FmX2lwc2V0X25vZGUnXQogIGlmIHR5cGUoanh3YWZfaXBzZXRfbm9kZSkgfj0gJ3RhYmxlJyAgdGhlbgogICAgcmV0dXJuCiAgZW5kCgogIGxvY2FsIGF1dGggPSBjb25mX2RhdGFbJ2F1dGgnXQogIGlmIHR5cGUoYXV0aCkgfj0gJ3N0cmluZycgIHRoZW4KICAgIHJldHVybgogIGVuZAoKICBsb2NhbCBmbG93X2lwX3JlZ2lvbl9ibG9jayA9IGNvbmZfZGF0YVsnZmxvd19pcF9yZWdpb25fYmxvY2snXQogIGlmIHR5cGUoZmxvd19pcF9yZWdpb25fYmxvY2spIH49ICdib29sZWFuJyAgdGhlbgogICAgbmd4LmxvZyhuZ3guRVJSLHR5cGUoZmxvd19pcF9yZWdpb25fYmxvY2spKQogICAgcmV0dXJuCiAgZW5kCgogIGxvY2FsIGZsb3dfcnVsZV9wcm90ZWN0aW9uID0gY29uZl9kYXRhWydmbG93X3J1bGVfcHJvdGVjdGlvbiddCiAgaWYgdHlwZShmbG93X3J1bGVfcHJvdGVjdGlvbikgfj0gJ3RhYmxlJyAgdGhlbgogICAgcmV0dXJuCiAgZW5kCiAgbG9jYWwgZmxvd19lbmdpbmVfcHJvdGVjdGlvbiA9IGNvbmZfZGF0YVsnZmxvd19lbmdpbmVfcHJvdGVjdGlvbiddCiAgaWYgdHlwZShmbG93X2VuZ2luZV9wcm90ZWN0aW9uKSB+PSAndGFibGUnICB0aGVuCiAgICByZXR1cm4KICBlbmQKCiAgbG9jYWwgZmxvd19pcF9yZWdpb25fYmxvY2tfcmVzdWx0ID0gbmd4LmN0eC5mbG93X2lwX3JlZ2lvbl9ibG9ja19yZXN1bHQKICBsb2NhbCBmbG93X3J1bGVfcHJvdGVjdGlvbl9yZXN1bHQgPSBuZ3guY3R4LmZsb3dfcnVsZV9wcm90ZWN0aW9uX3Jlc3VsdAogIGxvY2FsIGZsb3dfZW5naW5lX3Byb3RlY3Rpb25fcmVzdWx0ID0gbmd4LmN0eC5mbG93X2VuZ2luZV9wcm90ZWN0aW9uX3Jlc3VsdAogIGxvY2FsIGNoZWNrX3Jlc3VsdCAKICBpZiAgZmxvd19pcF9yZWdpb25fYmxvY2sgPT0gdHJ1ZSBhbmQgZmxvd19pcF9yZWdpb25fYmxvY2tfcmVzdWx0IHRoZW4KICAgIGNoZWNrX3Jlc3VsdCA9IHRydWUKICBlbmQKCiAgZm9yIF8sdiBpbiBpcGFpcnMoZmxvd19ydWxlX3Byb3RlY3Rpb24pIGRvIAogICAgaWYgZmxvd19ydWxlX3Byb3RlY3Rpb25fcmVzdWx0W3ZdIHRoZW4KICAgICAgY2hlY2tfcmVzdWx0ID0gdHJ1ZQogICAgZW5kCiAgZW5kCgogIGZvciBfLHYgaW4gaXBhaXJzKGZsb3dfZW5naW5lX3Byb3RlY3Rpb24pIGRvIAogICAgaWYgZmxvd19lbmdpbmVfcHJvdGVjdGlvbl9yZXN1bHRbdl0gdGhlbgogICAgICBjaGVja19yZXN1bHQgPSB0cnVlCiAgICBlbmQKICBlbmQKCiAgaWYgY2hlY2tfcmVzdWx0IHRoZW4KICAgIGxvY2FsIHNyY19pcCA9IG5neC5jdHguc3JjX2lwIG9yIG5neC52YXIucmVtb3RlX2FkZHIKICAgIGZvciBfLHYgaW4gaXBhaXJzKGp4d2FmX2lwc2V0X25vZGUpIGRvCiAgICAgIGxvY2FsIG9rLCBlcnIgPSBuZ3gudGltZXIuYXQoMCxzZW5kX2lwc2V0X2Jsb2NrLHYsc3JjX2lwLGF1dGgpCiAgICAgIGlmIG5vdCBvayB0aGVuCiAgICAgICAgaWYgZXJyIH49ICJwcm9jZXNzIGV4aXRpbmciIHRoZW4KICAgICAgICAgIG5neC5sb2cobmd4LkVSUiwgImZhaWxlZCB0byBjcmVhdGUgdGhlIHNlbmQgc2VuZF9pcHNldF9ibG9jayBodHRwIHRpbWVyOiAiLCBlcnIpCiAgICAgICAgZW5kCiAgICAgIGVuZAogICAgZW5kCiAgICBsb2NhbCB3YWZfbG9nID0gbmd4LmN0eC53YWZfbG9nIAogICAgd2FmX2xvZ1snd2FmX2FjdGlvbiddID0gImlwc2V0X2Jsb2NrIgogICAgbmd4LmN0eC53YWZfbG9nID0gd2FmX2xvZwogICAgdW5pZnlfYWN0aW9uLnJlamVjdF9yZXNwb25zZSgpCiAgICByZXR1cm4gdHJ1ZQogIGVuZAogIHJldHVybiAKZW5kCgpyZXR1cm4gX00=
说明:
需要写入经过BASE64编码后的代码,直接复制上面已经编码好的字符串即可。
# 默认配置
{"jxwaf_ipset_node":["1.1.1.1:6677","2.2.2.2:6677"],"auth":"aaaa","flow_ip_region_block":true,"flow_rule_protection":["test_rule"],"flow_engine_protection":["high_freq_cc_rate_check","high_freq_cc_count_check"]}
说明:
- jxwaf_ipset_node
部署了jxwaf_ipset_block的节点服务器地址,默认是6677端口
- auth
鉴权字段,在jxwaf_ipset_block程序启动会指定auth的值
- flow_ip_region_block
是否对IP区域封禁的匹配结果开启封禁,值为 true 或 false。
- flow_rule_protection
是否对流量防护规则的匹配结果开启封禁,值为数组类型,写入具体的流量防护规则名称。
- flow_engine_protection
是否对流量防护引擎的匹配结果开启封禁,值为数组类型,写入具体的流量防护模块名称。
具体值为:
high_freq_cc_rate_check
高频CC攻击防护-IP请求频率检测
high_freq_cc_count_check
高频CC攻击防护-IP请求次数检测
slow_cc_ip_count_check
慢速CC攻击防护-请求IP数量检测
slow_cc_domain_check
慢速CC攻击防护-回源保护机制
emergency_mode_check
无差别紧急防护
# get_real_ip组件
# 组件说明
该组件旨在通过 HTTP 请求头中的 X-REAL-IP 或 X-Forwarded-For 获取用户的真实 IP 地址,适用于在 HTTP 请求的回源 IP 地址无法确定的情况下使用。例如,腾讯云 CDN 就不提供回源 IP 地址段。
然而,在正常情况下,建议您使用控制台提供的 "WAF 前存在代理" 配置来实现真实 IP 的获取。通过在控制台中配置可信 IP 段,可以确保获得的 IP 地址是真实可靠的。
请注意:由于该组件获取的 IP 地址可能会因上一节点的转发 IP 不可靠而被欺骗,因此请谨慎使用。例如,攻击者通过自定义X-REAL-IP,达到欺骗攻击的效果。
# 代码分析
-- 定义一个局部表_M
local _M = {}
-- 设置版本号
_M.version = "jxwaf4"
-- 定义一个本地函数,用于检查IP地址的有效性
local function is_valid_ip(ip)
-- 将IP地址按照"."分隔成多个部分,并存储在表中
local parts = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
-- 如果分割出来的部分不等于4,则不是一个有效的IPv4地址
if #parts ~= 4 then
return false
end
-- 遍历每个部分,检查每个部分是否在0到255之间
for _, part in ipairs(parts) do
local num = tonumber(part)
if num < 0 or num > 255 then
return false
end
end
-- 检查IP是否为私有IP地址范围,如果是,则返回false
if parts[1] == "10"
or (parts[1] == "172" and tonumber(parts[2]) >= 16 and tonumber(parts[2]) <= 31)
or (parts[1] == "192" and parts[2] == "168") then
return false
end
-- 如果经过以上所有检查仍然有效,返回true
return true
end
-- 定义_M表中的一个方法,检查请求头中的IP地址
function _M.check()
-- 从请求头中获取“X-REAL-IP”字段
local xri_ip = ngx.req.get_headers()['X-REAL-IP']
-- 如果X-REAL-IP字段有效并且是一个有效的IP地址,将其存储在上下文中
if xri_ip and is_valid_ip(xri_ip) then
ngx.ctx.src_ip = xri_ip
return true
end
-- 如果“X-REAL-IP”字段无效或者不是有效IP地址,继续检查“X-Forwarded-For”字段
local xff = ngx.req.get_headers()['X-Forwarded-For']
local xff_ip = ngx.re.match(xff, [=[^\d{1,3}+\.\d{1,3}+\.\d{1,3}+\.\d{1,3}+]=], 'oj')[0]
-- 如果X-Forwarded-For字段有效并且是一个有效的IP地址,将其存储在上下文中
if xff_ip and is_valid_ip(xff_ip) then
ngx.ctx.src_ip = xff_ip
return true
end
-- 如果两个字段都没有有效IP地址,返回nil(Lua中nil等价于false)
return
end
-- 返回_M表
return _M
# 组件部署说明
JXWAF控制台 -> 防护管理 -> 基础组件 -> 新建组件
# 组件名称
get_real_ip
# 组件描述
通过HTTP请求头获取用户真实IP地址
# CODE
bG9jYWwgX00gPSB7fQpfTS52ZXJzaW9uID0gImp4d2FmNCIKbG9jYWwgZnVuY3Rpb24gaXNfdmFsaWRfaXAoaXApCiAgbG9jYWwgcGFydHMgPSB7aXA6bWF0Y2goIiglZCspJS4oJWQrKSUuKCVkKyklLiglZCspIil9CiAgaWYgI3BhcnRzIH49IDQgdGhlbgogICAgICByZXR1cm4gZmFsc2UKICBlbmQKICBmb3IgXywgcGFydCBpbiBpcGFpcnMocGFydHMpIGRvCiAgICAgIGxvY2FsIG51bSA9IHRvbnVtYmVyKHBhcnQpCiAgICAgIGlmIG51bSA8IDAgb3IgbnVtID4gMjU1IHRoZW4KICAgICAgICAgIHJldHVybiBmYWxzZQogICAgICBlbmQKICBlbmQKICBpZiBwYXJ0c1sxXSA9PSAiMTAiIG9yIChwYXJ0c1sxXSA9PSAiMTcyIiBhbmQgdG9udW1iZXIocGFydHNbMl0pID49IDE2IGFuZCB0b251bWJlcihwYXJ0c1syXSkgPD0gMzEpIG9yIChwYXJ0c1sxXSA9PSAiMTkyIiBhbmQgcGFydHNbMl0gPT0gIjE2OCIpIHRoZW4KICAgICAgcmV0dXJuIGZhbHNlCiAgZW5kCiAgcmV0dXJuIHRydWUKZW5kCmZ1bmN0aW9uIF9NLmNoZWNrKCkKICBsb2NhbCB4cmlfaXAgPSBuZ3gucmVxLmdldF9oZWFkZXJzKClbJ1gtUkVBTC1JUCddCiAgaWYgeHJpX2lwIGFuZCBpc192YWxpZF9pcCh4cmlfaXApIHRoZW4KICAgIG5neC5jdHguc3JjX2lwID0geHJpX2lwCiAgICByZXR1cm4gdHJ1ZQogIGVuZAogbG9jYWwgeGZmID0gbmd4LnJlcS5nZXRfaGVhZGVycygpWydYLUZvcndhcmRlZC1Gb3InXQogbG9jYWwgeGZmX2lwID0gbmd4LnJlLm1hdGNoKHhmZixbPVteXGR7MSwzfStcLlxkezEsM30rXC5cZHsxLDN9K1wuXGR7MSwzfStdPV0sJ29qJylbMF0KIGlmIHhmZl9pcCBhbmQgaXNfdmFsaWRfaXAoeGZmX2lwKSB0aGVuCiAgbmd4LmN0eC5zcmNfaXAgPSB4ZmZfaXAKICByZXR1cm4gdHJ1ZQogZW5kCiAgcmV0dXJuIAplbmQKcmV0dXJuIF9N
说明:
需要写入经过BASE64编码后的代码,直接复制上面已经编码好的字符串即可。
# 默认配置
{}
# inner_ip_health_check组件
# 组件说明
负载均衡会定期拨测内部节点,通过访问/health路径来确定节点是否存活,默认情况下,WAF对未配置的域名,返回是404,导致拨测出现异常。该组件会让所有访问该路径的请求都返回200,确保拨测正常。
# 代码分析
-- 定义一个空的表 `_M` 来存储模块的内容
local _M = {}
-- 定义模块的版本号
_M.version = "jxwaf4"
-- 定义一个函数 `check` 来执行健康检查
function _M.check()
-- 获取当前请求的 URI
local uri = ngx.var.uri
-- 如果当前请求的 URI 是 `/health`
if uri == '/health' then
-- 设置响应状态码为 200 (OK)
ngx.status = 200
-- 输出 "health check success" 作为响应内容
ngx.say('health check success')
-- 结束请求并返回状态码 200 (OK)
return ngx.exit(200)
end
-- 如果 URI 不是 `/health`,什么也不做,直接返回
return
end
-- 返回模块 `_M`,以便在其他文件中引入和使用
return _M
# 组件部署说明
JXWAF控制台 -> 防护管理 -> 基础组件 -> 新建组件
# 组件名称
inner_ip_health_check
# 组件描述
负载均衡健康检查支持组件
# CODE
bG9jYWwgX00gPSB7fQpfTS52ZXJzaW9uID0gImp4d2FmNCIKCmZ1bmN0aW9uIF9NLmNoZWNrKCkKICBsb2NhbCB1cmkgPSBuZ3gudmFyLnVyaSAKICBpZiB1cmkgPT0gJy9oZWFsdGgnIHRoZW4KICAgIG5neC5zdGF0dXMgPSAyMDAKICAgIG5neC5zYXkoJ2hlYWx0aCBjaGVjayBzdWNjZXNzJykKICAgIHJldHVybiBuZ3guZXhpdCgyMDApCiAgZW5kCiAgcmV0dXJuIAplbmQKcmV0dXJuIF9N
说明:
需要写入经过BASE64编码后的代码,直接复制上面已经编码好的字符串即可。
# 默认配置
{}