Files
ryan a1a997bcda feat(waf): complete composable rule orchestration
Add the React Flow rule editor, ordered graph APIs and runtime DAG execution.\n\nPublish rules only on OpenResty reload and reconcile checksum-driven IP group snapshots in bounded shared memory.
2026-07-13 14:17:15 +08:00

357 lines
15 KiB
Lua

local module_path = assert(WAF_IP_GROUPS_PATH, "WAF_IP_GROUPS_PATH is required")
local function assert_equal(actual, expected, message)
if actual ~= expected then
error((message or "values differ") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
end
end
local shared_data = {}
local locks = {}
local shared = {}
function shared:get(key) return shared_data[key] end
function shared:set(key, value) shared_data[key] = value return true end
function shared:delete(key) shared_data[key] = nil return true end
function shared:safe_set(key, value) return shared:set(key, value) end
function shared:add(key, value, ttl)
assert_equal(ttl, 4, "coordination lock TTL")
if locks[key] then return false end
locks[key] = value
return true
end
function shared:safe_add(key, value, ttl) return shared:add(key, value, ttl) end
local function advance_time() locks = {} end
local disk_checksum = "v1"
local disk_json = "valid-v1"
local checksum_reads = 0
local json_reads = 0
local timer_callbacks = {}
local function decode(raw)
if raw == "valid-v1" then
return { groups = { ["1"] = { enabled = true, ip_list = { "192.0.2.1" } } } }
end
if raw == "valid-v2" then
return { groups = { ["2"] = { enabled = true, ip_list = { "198.51.100.2" } } } }
end
if raw == "valid-v3" then
return { groups = { ["3"] = { enabled = true, ip_list = { "203.0.113.3" } } } }
end
return nil, "invalid json"
end
local function load_worker()
local worker = assert(loadfile(module_path))()
worker.init({
shared = shared,
timer_every = function(interval, callback)
assert_equal(interval, 5, "refresh interval")
timer_callbacks[#timer_callbacks + 1] = callback
return true
end,
read_checksum = function()
checksum_reads = checksum_reads + 1
return disk_checksum
end,
read_json = function()
json_reads = json_reads + 1
return disk_json
end,
decode = decode,
max_snapshot_bytes = 20 * 1024 * 1024,
})
return worker
end
local first = load_worker()
local second = load_worker()
assert_equal(#timer_callbacks, 2, "each worker schedules a refresh timer")
assert_equal(checksum_reads, 1, "one worker coordinates initial checksum read")
assert_equal(json_reads, 1, "one worker reads initial JSON")
assert_equal(first.current().groups["1"].ip_list[1], "192.0.2.1", "first worker adopts initial snapshot")
assert_equal(second.current().groups["1"].ip_list[1], "192.0.2.1", "second worker adopts initial snapshot")
local function tick_all()
advance_time()
for _, callback in ipairs(timer_callbacks) do callback(false) end
end
checksum_reads = 0
json_reads = 0
for _ = 1, 3 do tick_all() end
assert_equal(checksum_reads, 3, "stable 15 seconds reads checksum once per interval")
assert_equal(json_reads, 0, "unchanged checksum never reads JSON")
disk_checksum = "v2"
disk_json = "valid-v2"
tick_all()
assert_equal(json_reads, 1, "changed snapshot JSON is read once across workers")
assert_equal(first.current().groups["2"].ip_list[1], "198.51.100.2", "first worker adopts v2")
assert_equal(second.current().groups["2"].ip_list[1], "198.51.100.2", "second worker adopts v2")
disk_checksum = "v3"
disk_json = "valid-v3"
tick_all()
assert_equal(shared_data.ip_groups_previous_version, "v2", "previous pointer follows committed version")
assert_equal(shared_data["ip_groups_raw:v1"], nil, "snapshot older than previous is cleaned")
assert_equal(shared_data["ip_groups_raw:v2"], "valid-v2", "previous committed raw is retained")
assert_equal(shared_data["ip_groups_raw:v3"], "valid-v3", "current committed raw is retained")
disk_checksum = "v2"
disk_json = "valid-v2"
tick_all()
assert_equal(shared_data.ip_groups_version, "v2", "rollback checksum becomes current commit")
assert_equal(shared_data.ip_groups_previous_version, "v3", "rollback retains former current as previous")
assert_equal(shared_data["ip_groups_raw:v2"], "valid-v2", "rollback must not clean its new current raw")
assert_equal(shared_data["ip_groups_raw:v3"], "valid-v3", "rollback retains previous raw")
disk_checksum = "v4"
disk_json = "invalid-v4"
tick_all()
assert_equal(shared_data.ip_groups_version, "v2", "invalid update preserves shared version")
assert_equal(first.current().groups["2"].ip_list[1], "198.51.100.2", "invalid update preserves first worker")
assert_equal(second.current().groups["2"].ip_list[1], "198.51.100.2", "invalid update preserves second worker")
local reads_before_requests = checksum_reads + json_reads
for _ = 1, 20 do
assert_equal(first.current().groups["2"].enabled, true, "request reads worker-local object")
end
assert_equal(checksum_reads + json_reads, reads_before_requests, "current() performs zero file I/O")
timer_callbacks[1](true)
assert_equal(checksum_reads + json_reads, reads_before_requests, "premature timer performs zero file I/O")
local function test_failed_commit_never_exposes_unpublished_raw_to_new_worker()
local data = {}
local held_locks = {}
local callbacks = {}
local checksum = "v1"
local raw = "valid-v1"
local reads = 0
local fail_commit = false
local interleaved_worker
local load_regression_worker
local regression_shared = {}
function regression_shared:get(key) return data[key] end
function regression_shared:add(key, value)
if held_locks[key] then return false end
held_locks[key] = value
return true
end
function regression_shared:delete(key) data[key] = nil return true end
local function set_regression_value(key, value)
if key == "ip_groups_version" and fail_commit then
return false, "shared dictionary full"
end
data[key] = value
if fail_commit and string.sub(key, 1, #"ip_groups_raw") == "ip_groups_raw" and not interleaved_worker then
interleaved_worker = load_regression_worker()
end
return true
end
function regression_shared:set(key, value) return set_regression_value(key, value) end
function regression_shared:safe_set(key, value) return set_regression_value(key, value) end
function regression_shared:safe_add(key, value) return regression_shared:add(key, value) end
load_regression_worker = function()
local worker = assert(loadfile(module_path))()
assert(worker.init({
shared = regression_shared,
timer_every = function(_, callback) callbacks[#callbacks + 1] = callback return true end,
read_checksum = function() return checksum end,
read_json = function() reads = reads + 1 return raw end,
decode = decode,
max_snapshot_bytes = 20 * 1024 * 1024,
}))
return worker
end
local established_worker = load_regression_worker()
assert_equal(established_worker.current().groups["1"].ip_list[1], "192.0.2.1", "v1 is committed before failure")
held_locks = {}
reads = 0
checksum = "v2"
raw = "valid-v2"
fail_commit = true
callbacks[1](false)
assert_equal(reads, 1, "failed commit still reads changed JSON only once")
assert_equal(data.ip_groups_version, "v1", "failed pointer write preserves committed version")
assert_equal(data["ip_groups_raw:v2"], nil, "failed commit cleans only unpublished v2 raw")
assert_equal(established_worker.current().groups["1"].ip_list[1], "192.0.2.1", "existing worker preserves committed v1")
assert(interleaved_worker, "raw publication must interleave a newly initialized worker")
assert_equal(interleaved_worker.current().groups["2"], nil, "new worker must not expose unpublished v2")
assert_equal(interleaved_worker.current().groups["1"].ip_list[1], "192.0.2.1", "new worker must never adopt unpublished v2 raw")
end
test_failed_commit_never_exposes_unpublished_raw_to_new_worker()
local function test_capacity_failure_never_evicts_committed_snapshot()
local data = {
ip_groups_version = "v1",
ip_groups_previous_version = "v0",
["ip_groups_raw:v1"] = "valid-v1",
["ip_groups_raw:v0"] = "valid-v0",
}
local locks = {}
local callbacks = {}
local disk_checksum = "v1"
local disk_raw = "valid-v1"
local json_reads = 0
local ordinary_writes = 0
local warnings = {}
local dict = {}
function dict:get(key) return data[key] end
function dict:delete(key) data[key] = nil return true end
function dict:add(key, value)
if locks[key] then return false end
locks[key] = value
return true
end
function dict:safe_add(key, value) return dict:add(key, value) end
function dict:set(key, value)
ordinary_writes = ordinary_writes + 1
if key == "ip_groups_raw:v2" then
data = { [key] = value }
return true, nil, true
end
data[key] = value
return true, nil, false
end
function dict:safe_set(key, value)
if key == "ip_groups_raw:v2" then return nil, "no memory", false end
data[key] = value
return true, nil, false
end
local worker = assert(loadfile(module_path))()
assert(worker.init({
shared = dict,
timer_every = function(_, callback) callbacks[1] = callback return true end,
read_checksum = function() return disk_checksum end,
read_json = function() json_reads = json_reads + 1 return disk_raw end,
decode = decode,
max_snapshot_bytes = 20 * 1024 * 1024,
log_warning = function(message) warnings[#warnings + 1] = message end,
}))
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "worker starts from committed v1")
locks = {}
disk_checksum = "v2"
disk_raw = "valid-v2"
callbacks[1](false)
assert_equal(ordinary_writes, 0, "snapshot publication must never use evicting set")
assert_equal(json_reads, 1, "capacity failure reads changed JSON once")
assert_equal(data.ip_groups_version, "v1", "capacity failure preserves commit pointer")
assert_equal(data.ip_groups_previous_version, "v0", "capacity failure preserves previous metadata")
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "capacity failure preserves current raw")
assert_equal(data["ip_groups_raw:v0"], "valid-v0", "capacity failure preserves previous raw")
assert_equal(data["ip_groups_raw:v2"], nil, "capacity failure does not publish new raw")
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "capacity failure preserves worker-local snapshot")
assert_equal(#warnings, 1, "capacity failure is logged")
end
local function test_previous_metadata_failure_keeps_committed_snapshot_without_cleanup()
local data = {
ip_groups_version = "v1",
ip_groups_previous_version = "v0",
["ip_groups_raw:v1"] = "valid-v1",
["ip_groups_raw:v0"] = "valid-v0",
}
local locks = {}
local callback
local checksum = "v1"
local raw = "valid-v1"
local deletes = 0
local warnings = {}
local dict = {}
function dict:get(key) return data[key] end
function dict:delete(key) deletes = deletes + 1 data[key] = nil return true end
function dict:add(key, value)
if locks[key] then return false end
locks[key] = value
return true
end
function dict:safe_add(key, value) return dict:add(key, value) end
function dict:set(key, value) data[key] = value return true end
function dict:safe_set(key, value)
if key == "ip_groups_previous_version" then return nil, "no memory", false end
data[key] = value
return true, nil, false
end
local worker = assert(loadfile(module_path))()
assert(worker.init({
shared = dict,
timer_every = function(_, value) callback = value return true end,
read_checksum = function() return checksum end,
read_json = function() return raw end,
decode = decode,
max_snapshot_bytes = 20 * 1024 * 1024,
log_warning = function(message) warnings[#warnings + 1] = message end,
}))
locks = {}
checksum = "v2"
raw = "valid-v2"
callback(false)
assert_equal(data.ip_groups_version, "v2", "successful commit pointer remains authoritative")
assert_equal(data.ip_groups_previous_version, "v0", "failed previous metadata write is not forced")
assert_equal(data["ip_groups_raw:v2"], "valid-v2", "new committed raw remains")
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "old current raw remains when cleanup is skipped")
assert_equal(data["ip_groups_raw:v0"], "valid-v0", "old previous raw remains when cleanup is skipped")
assert_equal(deletes, 0, "previous metadata failure skips all cleanup")
assert_equal(worker.current().groups["2"].ip_list[1], "198.51.100.2", "worker adopts valid committed v2")
assert_equal(#warnings, 1, "previous metadata failure is logged")
end
local function test_oversized_raw_is_rejected_before_shared_publication()
local data = { ip_groups_version = "v1", ["ip_groups_raw:v1"] = "valid-v1" }
local locks = {}
local callback
local checksum = "v1"
local raw = "valid-v1"
local shared_writes = 0
local warnings = {}
local dict = {}
function dict:get(key) return data[key] end
function dict:delete(key) data[key] = nil return true end
function dict:add(key, value) if locks[key] then return false end locks[key] = value return true end
function dict:safe_add(key, value) return dict:add(key, value) end
function dict:set(key, value) shared_writes = shared_writes + 1 data[key] = value return true end
function dict:safe_set(key, value) shared_writes = shared_writes + 1 data[key] = value return true, nil, false end
local worker = assert(loadfile(module_path))()
assert(worker.init({
shared = dict,
timer_every = function(_, value) callback = value return true end,
read_checksum = function() return checksum end,
read_json = function() return raw end,
decode = decode,
max_snapshot_bytes = 4,
log_warning = function(message) warnings[#warnings + 1] = message end,
}))
locks = {}
checksum = "v2"
raw = "valid-v2"
callback(false)
assert_equal(shared_writes, 0, "oversized raw is rejected before shared writes")
assert_equal(data.ip_groups_version, "v1", "oversized raw preserves commit pointer")
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "oversized raw preserves committed data")
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "oversized raw preserves worker-local snapshot")
assert_equal(#warnings, 1, "oversized raw rejection is logged")
end
test_capacity_failure_never_evicts_committed_snapshot()
test_previous_metadata_failure_keeps_committed_snapshot_without_cleanup()
test_oversized_raw_is_rejected_before_shared_publication()
return true