Files
OpenFlare/internal/apps/agent/nginx/waf_ip_groups.lua
T
ryan 1a7e5e6c41 perf(waf): IP 匹配改为索引查询(ipmatcher / 预编译)
加载时编译 IP 组与节点 IP/CIDR 索引,优先 resty.ipmatcher 基数树,
否则 exact 哈希 + 预解析 CIDR,避免大名单线性扫描打满边缘 CPU。
2026-07-19 14:48:07 +08:00

152 lines
5.4 KiB
Lua

local _M = {}
local current_groups = { groups = {} }
local current_version
local initialized = false
local shared
local read_checksum
local read_json
local decode
local log_warning
local max_snapshot_bytes
local refresh_lock_key = "ip_groups_refresh_lock"
local raw_snapshot_prefix = "ip_groups_raw:"
local version_key = "ip_groups_version"
local previous_version_key = "ip_groups_previous_version"
local function warn(message, err, forcible)
local suffix = err and (": " .. tostring(err)) or ""
if forcible then suffix = suffix .. " (forcible eviction refused)" end
pcall(log_warning, "openflare WAF IP group refresh " .. message .. suffix)
end
local function safe_set(key, value, description)
local ok, err, forcible = shared:safe_set(key, value)
if ok ~= true or forcible == true then
warn(description, err, forcible)
return false
end
return true
end
local function read_file(path)
local file, err = io.open(path, "rb")
if not file then return nil, err end
local content = file:read("*a")
file:close()
return content
end
local function valid_snapshot(snapshot)
return type(snapshot) == "table" and type(snapshot.groups) == "table"
end
local function decode_snapshot(raw)
if type(raw) ~= "string" or raw == "" then return nil end
local called, snapshot = pcall(decode, raw)
if not called or not valid_snapshot(snapshot) then return nil end
return snapshot
end
local function refresh_from_checksum()
local called, checksum = pcall(read_checksum)
if not called or type(checksum) ~= "string" then return end
checksum = string.match(checksum, "^%s*(.-)%s*$")
local committed_version = shared:get(version_key)
if checksum == "" or checksum == committed_version then return end
local json_called, raw = pcall(read_json)
if not json_called then
warn("JSON read failed", raw)
return
end
if type(raw) ~= "string" or #raw > max_snapshot_bytes then
warn("snapshot exceeds maximum " .. tostring(max_snapshot_bytes) .. " bytes")
return
end
if not decode_snapshot(raw) then return end
local raw_key = raw_snapshot_prefix .. checksum
local existing_raw = shared:get(raw_key)
local published_new_raw = false
if existing_raw == nil then
if not safe_set(raw_key, raw, "raw publication failed") then return end
published_new_raw = true
elseif existing_raw ~= raw then
return
end
if not safe_set(version_key, checksum, "commit pointer publication failed") then
if published_new_raw then shared:delete(raw_key) end
return
end
local previous_version = shared:get(previous_version_key)
if type(committed_version) == "string" and committed_version ~= "" and committed_version ~= checksum then
if not safe_set(previous_version_key, committed_version, "previous version metadata publication failed") then return end
if type(previous_version) == "string" and previous_version ~= "" and
previous_version ~= committed_version and previous_version ~= checksum then
shared:delete(raw_snapshot_prefix .. previous_version)
end
end
end
local function adopt_shared_snapshot_if_changed()
local version = shared:get(version_key)
if type(version) ~= "string" or version == "" or version == current_version then return end
local snapshot = decode_snapshot(shared:get(raw_snapshot_prefix .. version))
if not snapshot then return end
-- Matchers are compiled lazily in waf.runtime (resty.ipmatcher / fallback index).
current_groups = snapshot
current_version = version
end
local function tick(premature)
if premature then return end
local locked, lock_error, forcible = shared:safe_add(refresh_lock_key, true, 4)
if forcible == true then
warn("coordination lock refused forcible eviction", lock_error, true)
locked = false
elseif not locked and lock_error and lock_error ~= "exists" then
warn("coordination lock failed", lock_error)
end
if locked then refresh_from_checksum() end
adopt_shared_snapshot_if_changed()
end
function _M.init(options)
if initialized then return true end
options = options or {}
local runtime_dir = options.runtime_dir or "__OPENFLARE_RUNTIME_CONFIG_DIR__"
shared = options.shared or (ngx.shared and ngx.shared.openflare_waf_ip_groups)
assert(shared, "openflare_waf_ip_groups shared dictionary is required")
max_snapshot_bytes = options.max_snapshot_bytes or tonumber("__OPENFLARE_WAF_IP_GROUPS_MAX_SNAPSHOT_BYTES__")
assert(max_snapshot_bytes and max_snapshot_bytes > 0, "WAF IP group maximum snapshot size is required")
log_warning = options.log_warning or function(message)
if ngx and ngx.log then ngx.log(ngx.WARN, message) end
end
read_checksum = options.read_checksum or function()
return read_file(runtime_dir .. "/waf_ip_groups.json.checksum")
end
read_json = options.read_json or function()
return read_file(runtime_dir .. "/waf_ip_groups.json")
end
if options.decode then
decode = options.decode
else
local cjson = require("cjson.safe")
decode = cjson.decode
end
local timer_every = options.timer_every or ngx.timer.every
local ok, err = timer_every(5, tick)
if not ok then return nil, err end
initialized = true
tick(false)
return true
end
function _M.current()
return current_groups
end
return _M