mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
1a7e5e6c41
加载时编译 IP 组与节点 IP/CIDR 索引,优先 resty.ipmatcher 基数树, 否则 exact 哈希 + 预解析 CIDR,避免大名单线性扫描打满边缘 CPU。
152 lines
5.4 KiB
Lua
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
|