mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 02:06:37 +08:00
perf(waf): IP 匹配改为索引查询(ipmatcher / 预编译)
加载时编译 IP 组与节点 IP/CIDR 索引,优先 resty.ipmatcher 基数树, 否则 exact 哈希 + 预解析 CIDR,避免大名单线性扫描打满边缘 CPU。
This commit is contained in:
@@ -28,6 +28,7 @@ FROM openresty/openresty:alpine
|
|||||||
RUN apk add --no-cache ca-certificates tzdata perl libmaxminddb su-exec libcap \
|
RUN apk add --no-cache ca-certificates tzdata perl libmaxminddb su-exec libcap \
|
||||||
&& ln -sf /usr/lib/libmaxminddb.so.0 /usr/lib/libmaxminddb.so \
|
&& ln -sf /usr/lib/libmaxminddb.so.0 /usr/lib/libmaxminddb.so \
|
||||||
&& opm get anjia0532/lua-resty-maxminddb \
|
&& opm get anjia0532/lua-resty-maxminddb \
|
||||||
|
&& opm get api7/lua-resty-ipmatcher \
|
||||||
&& addgroup -S openflare \
|
&& addgroup -S openflare \
|
||||||
&& adduser -S -G openflare -H -h /data -s /sbin/nologin openflare \
|
&& adduser -S -G openflare -H -h /data -s /sbin/nologin openflare \
|
||||||
&& mkdir -p /etc/openflare /data \
|
&& mkdir -p /etc/openflare /data \
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ sidebar: false
|
|||||||
- WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。
|
- WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。
|
||||||
- WAF 规则编辑器支持一键格式化布局,按流程层次自动整理节点位置。
|
- WAF 规则编辑器支持一键格式化布局,按流程层次自动整理节点位置。
|
||||||
- 优化边缘 WAF「安全防护」与「UA 检查」热路径:SQL/命令/XSS 等仅扫描 Query、Cookie、Referer 与有限 Body,避免对全部请求头做特征匹配;路径检测不再重复扫描完整 `request_uri`;无请求体时跳过 Body 读取;UA 分类仅小写一次并加速白名单匹配,显著降低开启基础防护时的 CPU 占用。
|
- 优化边缘 WAF「安全防护」与「UA 检查」热路径:SQL/命令/XSS 等仅扫描 Query、Cookie、Referer 与有限 Body,避免对全部请求头做特征匹配;路径检测不再重复扫描完整 `request_uri`;无请求体时跳过 Body 读取;UA 分类仅小写一次并加速白名单匹配,显著降低开启基础防护时的 CPU 占用。
|
||||||
|
- 优化边缘 WAF「IP 匹配」:IP 组与节点 IP/CIDR 在加载时编译为索引(优先 `lua-resty-ipmatcher` 基数树,否则 exact 哈希 + 预解析 CIDR),查询与名单规模解耦,避免大名单线性扫描打满 CPU。
|
||||||
|
|
||||||
## [v3.4.0] - 2026-07-19
|
## [v3.4.0] - 2026-07-19
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# WAF IP 匹配:Radix / lua-resty-ipmatcher
|
||||||
|
|
||||||
|
## 1. 目标与背景 (Goal & Context)
|
||||||
|
|
||||||
|
* **需求背景**:`ip_match` 对 IP 组 `ip_list` 做线性扫描,且每行强制 `ipv6_equal` + `ip_in_cidr`,大名单(订阅/自动规则可达万~十万级)时压测 RPS 约 65、OpenResty CPU 打满。
|
||||||
|
* **开发范围 (Scope)**:
|
||||||
|
* **必做**:边缘热路径改为预处理索引 + O(W) 查询;IP 组快照加载时编译;节点内联 `ips`/`cidrs` 同样编译;Agent 镜像安装 `lua-resty-ipmatcher`;规格与 changelog。
|
||||||
|
* **Out of Scope**:控制面协议变更、改 IP 组存储格式、Geo 匹配优化。
|
||||||
|
|
||||||
|
## 2. 设计与决策 (Design & Decisions)
|
||||||
|
|
||||||
|
* **选型**:OpenResty 使用 `resty.ipmatcher`(底层 Radix,支持 IP 与 CIDR 统一;可用 `match_bin(binary_remote_addr)`)。
|
||||||
|
* **编译时机**:
|
||||||
|
* IP 组:`waf.ip_groups` 采纳新快照时为每组 `ip_list` 建 matcher,挂到 `group._matcher`。
|
||||||
|
* 节点 `ips`/`cidrs`:首次匹配时合并列表建 matcher,用 weak 缓存或按 config 引用缓存。
|
||||||
|
* **回退**:`require("resty.ipmatcher")` 失败时用纯 Lua「exact set + 预解析 CIDR」回退(测试 / 未装 opm 的本地 OpenResty),避免回归到每行 IPv6 全解析。
|
||||||
|
* **不引入**:手写纯 Lua 十万节点 table 树作为生产主路径(内存与 GC 差)。
|
||||||
|
|
||||||
|
## 3. 具体修改文件清单 (Proposed Changes)
|
||||||
|
|
||||||
|
### 边缘 Agent 与 OpenResty
|
||||||
|
|
||||||
|
* #### [MODIFY] `docker/Dockerfile.agent`
|
||||||
|
* `opm get api7/lua-resty-ipmatcher`(与 maxminddb 并列)。
|
||||||
|
* #### [MODIFY] `internal/apps/agent/nginx/waf_runtime.lua`
|
||||||
|
* 编译/查询 helper;重写 `matches_ip_values`。
|
||||||
|
* #### [MODIFY] `internal/apps/agent/nginx/waf_ip_groups.lua`
|
||||||
|
* 无需在刷新模块内编译;快照采纳后由 `waf.runtime` 惰性编译 `group._matcher`。
|
||||||
|
* #### [MODIFY] `internal/apps/agent/nginx/waf_runtime_spec.lua` / `waf_ip_groups_spec.lua`
|
||||||
|
* 覆盖 exact/CIDR/IPv6/组 miss;大名单语义 smoke。
|
||||||
|
* #### [MODIFY] `docs/changelog/index.md`、相关设计/plan 备注
|
||||||
|
|
||||||
|
## 4. 验证计划 (Verification Plan)
|
||||||
|
|
||||||
|
* `go test ./internal/apps/agent/nginx/ -count=1`
|
||||||
|
* 重建 Agent 镜像后压测:三组大名单 miss 路径 CPU/RPS 对比。
|
||||||
@@ -95,6 +95,7 @@ local function adopt_shared_snapshot_if_changed()
|
|||||||
if type(version) ~= "string" or version == "" or version == current_version then return end
|
if type(version) ~= "string" or version == "" or version == current_version then return end
|
||||||
local snapshot = decode_snapshot(shared:get(raw_snapshot_prefix .. version))
|
local snapshot = decode_snapshot(shared:get(raw_snapshot_prefix .. version))
|
||||||
if not snapshot then return end
|
if not snapshot then return end
|
||||||
|
-- Matchers are compiled lazily in waf.runtime (resty.ipmatcher / fallback index).
|
||||||
current_groups = snapshot
|
current_groups = snapshot
|
||||||
current_version = version
|
current_version = version
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -139,29 +139,36 @@ local function config_geo_requirements(config)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function _M.init(options)
|
function _M.init(options)
|
||||||
if rules_config then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local runtime_dir = options.runtime_dir or "__OPENFLARE_RUNTIME_CONFIG_DIR__"
|
local runtime_dir = options.runtime_dir or "__OPENFLARE_RUNTIME_CONFIG_DIR__"
|
||||||
|
-- Always apply explicit test/runtime injections; only short-circuit cold disk load once.
|
||||||
if options.config then
|
if options.config then
|
||||||
rules_config = options.config
|
rules_config = options.config
|
||||||
else
|
elseif not rules_config then
|
||||||
local err
|
local err
|
||||||
rules_config, err = load_json(runtime_dir .. "/waf_config.json")
|
rules_config, err = load_json(runtime_dir .. "/waf_config.json")
|
||||||
assert(rules_config, "load waf_config.json failed: " .. tostring(err))
|
assert(rules_config, "load waf_config.json failed: " .. tostring(err))
|
||||||
end
|
end
|
||||||
if options.ip_groups then
|
if options.ip_groups then
|
||||||
ip_groups_config = options.ip_groups
|
ip_groups_config = options.ip_groups
|
||||||
else
|
-- Drop stale compiled matchers when tests inject a fresh snapshot table.
|
||||||
|
local groups = (ip_groups_config.groups or {})
|
||||||
|
for _, group in pairs(groups) do
|
||||||
|
if type(group) == "table" then group._matcher = nil end
|
||||||
|
end
|
||||||
|
elseif not ip_groups_config and not ip_groups_runtime then
|
||||||
ip_groups_runtime = options.ip_groups_runtime or require("waf.ip_groups")
|
ip_groups_runtime = options.ip_groups_runtime or require("waf.ip_groups")
|
||||||
local initialized, init_error = ip_groups_runtime.init({ runtime_dir = runtime_dir })
|
local initialized, init_error = ip_groups_runtime.init({ runtime_dir = runtime_dir })
|
||||||
assert(initialized, "initialize WAF IP groups failed: " .. tostring(init_error))
|
assert(initialized, "initialize WAF IP groups failed: " .. tostring(init_error))
|
||||||
end
|
end
|
||||||
pow_runtime = options.pow or require("pow.runtime")
|
if options.pow then
|
||||||
|
pow_runtime = options.pow
|
||||||
|
elseif not pow_runtime then
|
||||||
|
pow_runtime = require("pow.runtime")
|
||||||
|
end
|
||||||
if options.geo_lookup then
|
if options.geo_lookup then
|
||||||
geo_lookup = options.geo_lookup
|
geo_lookup = options.geo_lookup
|
||||||
else
|
elseif not geo_lookup then
|
||||||
local uses_geo, uses_region = config_geo_requirements(rules_config)
|
local uses_geo, uses_region = config_geo_requirements(rules_config)
|
||||||
if uses_geo then
|
if uses_geo then
|
||||||
init_geo_databases(
|
init_geo_databases(
|
||||||
@@ -240,55 +247,173 @@ local function parse_ipv6(value)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ipv6_equal(left, right)
|
local function ipv6_key(groups)
|
||||||
left, right = parse_ipv6(left), parse_ipv6(right)
|
return table.concat(groups, ":")
|
||||||
if not left or not right then return false end
|
|
||||||
for index = 1, 8 do
|
|
||||||
if left[index] ~= right[index] then return false end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ip_in_cidr(ip, cidr)
|
local function preparse_cidr(cidr)
|
||||||
local base, bits = string.match(cidr or "", "^([^/]+)/(%d+)$")
|
local base, bits = string.match(cidr or "", "^([^/]+)/(%d+)$")
|
||||||
bits = tonumber(bits)
|
bits = tonumber(bits)
|
||||||
if not base or not bits then return false end
|
if not base or not bits then return nil end
|
||||||
local ip_number, base_number = parse_ipv4(ip), parse_ipv4(base)
|
local base_v4 = parse_ipv4(base)
|
||||||
if ip_number and base_number then
|
if base_v4 then
|
||||||
if bits < 0 or bits > 32 then return false end
|
if bits < 0 or bits > 32 then return nil end
|
||||||
if bits == 0 then return true end
|
if bits == 0 then return { kind = "v4", bits = 0, network = 0, size = 0 } end
|
||||||
local size = 2 ^ (32 - bits)
|
local size = 2 ^ (32 - bits)
|
||||||
return ip_number - (ip_number % size) == base_number - (base_number % size)
|
return { kind = "v4", bits = bits, network = base_v4 - (base_v4 % size), size = size }
|
||||||
end
|
end
|
||||||
local ip_groups, base_groups = parse_ipv6(ip), parse_ipv6(base)
|
local base_v6 = parse_ipv6(base)
|
||||||
if not ip_groups or not base_groups or bits < 0 or bits > 128 then return false end
|
if not base_v6 or bits < 0 or bits > 128 then return nil end
|
||||||
local full_groups, remaining_bits = math.floor(bits / 16), bits % 16
|
return { kind = "v6", bits = bits, groups = base_v6 }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ipv4_in_preparsed(ip_number, cidr)
|
||||||
|
if cidr.bits == 0 then return true end
|
||||||
|
return ip_number - (ip_number % cidr.size) == cidr.network
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ipv6_in_preparsed(ip_groups, cidr)
|
||||||
|
local full_groups, remaining_bits = math.floor(cidr.bits / 16), cidr.bits % 16
|
||||||
for index = 1, full_groups do
|
for index = 1, full_groups do
|
||||||
if ip_groups[index] ~= base_groups[index] then return false end
|
if ip_groups[index] ~= cidr.groups[index] then return false end
|
||||||
end
|
end
|
||||||
if remaining_bits > 0 then
|
if remaining_bits > 0 then
|
||||||
local size = 2 ^ (16 - remaining_bits)
|
local size = 2 ^ (16 - remaining_bits)
|
||||||
local index = full_groups + 1
|
local index = full_groups + 1
|
||||||
if math.floor(ip_groups[index] / size) ~= math.floor(base_groups[index] / size) then return false end
|
if math.floor(ip_groups[index] / size) ~= math.floor(cidr.groups[index] / size) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Prefer resty.ipmatcher (C radix). Fallback: exact hash + pre-parsed CIDR list only.
|
||||||
|
local resty_ipmatcher
|
||||||
|
local resty_ipmatcher_loaded = false
|
||||||
|
|
||||||
|
local function load_resty_ipmatcher()
|
||||||
|
if resty_ipmatcher_loaded then return resty_ipmatcher end
|
||||||
|
resty_ipmatcher_loaded = true
|
||||||
|
local ok, mod = pcall(require, "resty.ipmatcher")
|
||||||
|
if ok and type(mod) == "table" and type(mod.new) == "function" then
|
||||||
|
resty_ipmatcher = mod
|
||||||
|
else
|
||||||
|
resty_ipmatcher = nil
|
||||||
|
end
|
||||||
|
return resty_ipmatcher
|
||||||
|
end
|
||||||
|
|
||||||
|
local empty_ip_matcher = {
|
||||||
|
empty = true,
|
||||||
|
match = function() return false end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function compile_fallback_ip_matcher(entries)
|
||||||
|
local exact, cidrs = {}, {}
|
||||||
|
for _, item in ipairs(entries) do
|
||||||
|
if string.find(item, "/", 1, true) then
|
||||||
|
local parsed = preparse_cidr(item)
|
||||||
|
if parsed then cidrs[#cidrs + 1] = parsed end
|
||||||
|
else
|
||||||
|
exact[item] = true
|
||||||
|
local v6 = parse_ipv6(item)
|
||||||
|
if v6 then exact["v6:" .. ipv6_key(v6)] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
empty = false,
|
||||||
|
match = function(_, ip, _bin, ip_v4, ip_v6)
|
||||||
|
if exact[ip] then return true end
|
||||||
|
if ip_v6 and exact["v6:" .. ipv6_key(ip_v6)] then return true end
|
||||||
|
if not ip_v4 and not ip_v6 then
|
||||||
|
ip_v4 = parse_ipv4(ip)
|
||||||
|
if not ip_v4 then ip_v6 = parse_ipv6(ip) end
|
||||||
|
end
|
||||||
|
for _, cidr in ipairs(cidrs) do
|
||||||
|
if cidr.kind == "v4" and ip_v4 and ipv4_in_preparsed(ip_v4, cidr) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if cidr.kind == "v6" and ip_v6 and ipv6_in_preparsed(ip_v6, cidr) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function compile_ip_matcher(entries)
|
||||||
|
local list = {}
|
||||||
|
for _, item in ipairs(array_or_empty(entries)) do
|
||||||
|
if type(item) == "string" and item ~= "" then
|
||||||
|
list[#list + 1] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #list == 0 then return empty_ip_matcher end
|
||||||
|
|
||||||
|
local mod = load_resty_ipmatcher()
|
||||||
|
if mod then
|
||||||
|
local matcher, err = mod.new(list)
|
||||||
|
if matcher then
|
||||||
|
return {
|
||||||
|
empty = false,
|
||||||
|
match = function(_, ip, bin_ip)
|
||||||
|
if bin_ip and matcher.match_bin then
|
||||||
|
local ok = matcher:match_bin(bin_ip)
|
||||||
|
if ok then return true end
|
||||||
|
end
|
||||||
|
return matcher:match(ip) == true
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
warn_rate_limited("_ipmatcher_new_failed", "openflare waf ipmatcher.new failed: ", err)
|
||||||
|
end
|
||||||
|
return compile_fallback_ip_matcher(list)
|
||||||
|
end
|
||||||
|
|
||||||
|
local node_ip_matcher_cache = setmetatable({}, { __mode = "k" })
|
||||||
|
|
||||||
|
local function matcher_for_node_ip_config(config)
|
||||||
|
config = config or {}
|
||||||
|
local cached = node_ip_matcher_cache[config]
|
||||||
|
if cached then return cached end
|
||||||
|
local entries = {}
|
||||||
|
for _, item in ipairs(array_or_empty(config.ips)) do entries[#entries + 1] = item end
|
||||||
|
for _, item in ipairs(array_or_empty(config.cidrs)) do entries[#entries + 1] = item end
|
||||||
|
local matcher = compile_ip_matcher(entries)
|
||||||
|
node_ip_matcher_cache[config] = matcher
|
||||||
|
return matcher
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matcher_for_ip_group(group)
|
||||||
|
if type(group) ~= "table" then return empty_ip_matcher end
|
||||||
|
if group._matcher then return group._matcher end
|
||||||
|
group._matcher = compile_ip_matcher(group.ip_list)
|
||||||
|
return group._matcher
|
||||||
|
end
|
||||||
|
|
||||||
local function matches_ip_values(config, ip)
|
local function matches_ip_values(config, ip)
|
||||||
for _, item in ipairs(array_or_empty(config.ips)) do
|
if type(ip) ~= "string" or ip == "" then return false end
|
||||||
if item == ip or ipv6_equal(item, ip) then return true end
|
local bin_ip = ngx.var and ngx.var.binary_remote_addr or nil
|
||||||
|
local ip_v4, ip_v6
|
||||||
|
-- Parse client IP once for pure-Lua fallback CIDR/exact-v6 paths.
|
||||||
|
if not load_resty_ipmatcher() then
|
||||||
|
ip_v4 = parse_ipv4(ip)
|
||||||
|
if not ip_v4 then ip_v6 = parse_ipv6(ip) end
|
||||||
end
|
end
|
||||||
for _, cidr in ipairs(array_or_empty(config.cidrs)) do
|
|
||||||
if ip_in_cidr(ip, cidr) then return true end
|
local node_matcher = matcher_for_node_ip_config(config)
|
||||||
|
if not node_matcher.empty and node_matcher:match(ip, bin_ip, ip_v4, ip_v6) then
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
local snapshot = ip_groups_config or ip_groups_runtime.current()
|
|
||||||
|
local snapshot = ip_groups_config or (ip_groups_runtime and ip_groups_runtime.current())
|
||||||
local groups = (snapshot or {}).groups or {}
|
local groups = (snapshot or {}).groups or {}
|
||||||
for _, id in ipairs(array_or_empty(config.ip_group_ids)) do
|
for _, id in ipairs(array_or_empty(config.ip_group_ids)) do
|
||||||
local group = groups[tostring(id)]
|
local group = groups[tostring(id)]
|
||||||
if group and group.enabled then
|
if group and group.enabled then
|
||||||
for _, item in ipairs(array_or_empty(group.ip_list)) do
|
local matcher = matcher_for_ip_group(group)
|
||||||
if item == ip or ipv6_equal(item, ip) or ip_in_cidr(ip, item) then return true end
|
if matcher:match(ip, bin_ip, ip_v4, ip_v6) then return true end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -811,4 +936,12 @@ function _M.debug_execute_graph(graph)
|
|||||||
return execute_graph(graph)
|
return execute_graph(graph)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _M.debug_compile_ip_matcher(entries)
|
||||||
|
return compile_ip_matcher(entries)
|
||||||
|
end
|
||||||
|
|
||||||
|
function _M.debug_matches_ip_values(config, ip)
|
||||||
|
return matches_ip_values(config or {}, ip or "")
|
||||||
|
end
|
||||||
|
|
||||||
return _M
|
return _M
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ local function assert_equal(actual, expected, message)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local output
|
-- Stable tables: never rebind `output` (closures capture the upvalue slot; rebinding
|
||||||
local pow_calls
|
-- can leave stale fields visible under gopher-lua across long test sequences).
|
||||||
local pow_results
|
local output = {}
|
||||||
|
local pow_calls = {}
|
||||||
|
local pow_results = {}
|
||||||
local shared_keys = {}
|
local shared_keys = {}
|
||||||
local logs = {}
|
local logs = {}
|
||||||
|
|
||||||
|
local function clear_output()
|
||||||
|
output.exit = nil
|
||||||
|
output.body = nil
|
||||||
|
output.log = nil
|
||||||
|
end
|
||||||
|
|
||||||
ngx = {
|
ngx = {
|
||||||
WARN = "WARN",
|
WARN = "WARN",
|
||||||
ERR = "ERR",
|
ERR = "ERR",
|
||||||
@@ -93,7 +101,7 @@ local function reset_request(site, ip, uri, is_internal, user_agent)
|
|||||||
ngx.ctx = {}
|
ngx.ctx = {}
|
||||||
ngx.header = {}
|
ngx.header = {}
|
||||||
ngx.status = nil
|
ngx.status = nil
|
||||||
output = {}
|
clear_output()
|
||||||
pow_calls = {}
|
pow_calls = {}
|
||||||
pow_results = {}
|
pow_results = {}
|
||||||
ngx.req = {
|
ngx.req = {
|
||||||
@@ -569,7 +577,6 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "missing UA with require_ua should block")
|
assert_equal(output.exit, 403, "missing UA with require_ua should block")
|
||||||
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "present UA with require_ua should allow")
|
assert_equal(output.exit, nil, "present UA with require_ua should allow")
|
||||||
|
|
||||||
@@ -589,11 +596,9 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "abnormal UA should be blocked")
|
assert_equal(output.exit, 403, "abnormal UA should be blocked")
|
||||||
reset_request("ua-site", nil, nil, nil, bot_ua)
|
reset_request("ua-site", nil, nil, nil, bot_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "search bot should not be abnormal when bots switch is off")
|
assert_equal(output.exit, nil, "search bot should not be abnormal when bots switch is off")
|
||||||
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "normal browser should pass abnormal check")
|
assert_equal(output.exit, nil, "normal browser should pass abnormal check")
|
||||||
|
|
||||||
@@ -608,7 +613,6 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "custom regex should block matching UA")
|
assert_equal(output.exit, 403, "custom regex should block matching UA")
|
||||||
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "custom regex should allow non-matching UA")
|
assert_equal(output.exit, nil, "custom regex should allow non-matching UA")
|
||||||
|
|
||||||
@@ -620,7 +624,6 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "Safari should miss Chrome whitelist")
|
assert_equal(output.exit, 403, "Safari should miss Chrome whitelist")
|
||||||
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
reset_request("ua-site", nil, nil, nil, chrome_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "Chrome should hit whitelist")
|
assert_equal(output.exit, nil, "Chrome should hit whitelist")
|
||||||
|
|
||||||
@@ -636,7 +639,6 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "Chrome desktop should fail Chrome+iOS and")
|
assert_equal(output.exit, 403, "Chrome desktop should fail Chrome+iOS and")
|
||||||
reset_request("ua-site", nil, nil, nil, safari_ios_ua)
|
reset_request("ua-site", nil, nil, nil, safari_ios_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, 403, "Safari iOS should fail Chrome+iOS and")
|
assert_equal(output.exit, 403, "Safari iOS should fail Chrome+iOS and")
|
||||||
|
|
||||||
@@ -652,7 +654,6 @@ local function test_ua_check_require_block_and_whitelist()
|
|||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "Chrome desktop should pass Chrome|iOS or")
|
assert_equal(output.exit, nil, "Chrome desktop should pass Chrome|iOS or")
|
||||||
reset_request("ua-site", nil, nil, nil, safari_ios_ua)
|
reset_request("ua-site", nil, nil, nil, safari_ios_ua)
|
||||||
output = {}
|
|
||||||
runtime.check()
|
runtime.check()
|
||||||
assert_equal(output.exit, nil, "Safari iOS should pass Chrome|iOS or")
|
assert_equal(output.exit, nil, "Safari iOS should pass Chrome|iOS or")
|
||||||
end
|
end
|
||||||
@@ -904,7 +905,60 @@ local function test_security_check_path_and_sql()
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function test_ip_matcher_index_miss_and_hit()
|
||||||
|
local runtime = load_runtime({ rule_groups = {}, bindings = {} }, {
|
||||||
|
ip_groups = {
|
||||||
|
groups = {
|
||||||
|
["1"] = {
|
||||||
|
enabled = true,
|
||||||
|
ip_list = {
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"203.0.113.50",
|
||||||
|
"2001:db8:1::/48",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local many = {}
|
||||||
|
for i = 1, 5000 do
|
||||||
|
many[i] = string.format("198.51.100.%d", (i % 254) + 1)
|
||||||
|
end
|
||||||
|
many[#many + 1] = "198.51.100.0/24"
|
||||||
|
local matcher = runtime.debug_compile_ip_matcher(many)
|
||||||
|
assert_equal(matcher:match("203.0.113.1"), false, "large list miss")
|
||||||
|
assert_equal(matcher:match("198.51.100.9"), true, "large list CIDR or exact hit")
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
runtime.debug_matches_ip_values({ ip_group_ids = { 1 } }, "203.0.113.50"),
|
||||||
|
true,
|
||||||
|
"group exact hit"
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
runtime.debug_matches_ip_values({ ip_group_ids = { 1 } }, "10.1.2.3"),
|
||||||
|
true,
|
||||||
|
"group CIDR hit"
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
runtime.debug_matches_ip_values({ ip_group_ids = { 1 } }, "198.51.100.1"),
|
||||||
|
false,
|
||||||
|
"group miss"
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
runtime.debug_matches_ip_values({ ips = { "192.0.2.9" }, cidrs = { "198.51.100.0/24" } }, "198.51.100.20"),
|
||||||
|
true,
|
||||||
|
"node cidr hit via compiled matcher"
|
||||||
|
)
|
||||||
|
assert_equal(
|
||||||
|
runtime.debug_matches_ip_values({ ips = { "2001:db8::1" } }, "2001:0db8:0:0:0:0:0:1"),
|
||||||
|
true,
|
||||||
|
"node ipv6 canonical exact"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test_ua_check_require_block_and_whitelist()
|
test_ua_check_require_block_and_whitelist()
|
||||||
test_security_check_path_and_sql()
|
test_security_check_path_and_sql()
|
||||||
|
test_ip_matcher_index_miss_and_hit()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
Reference in New Issue
Block a user