# 组件分享

# 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编码后的代码,直接复制上面已经编码好的字符串即可。

# 默认配置

{}