comparison mod_firewall/mod_firewall.lua @ 2578:6dbd07f9a868

mod_firewall: Various improvements allowing dynamic load/reload/unload of scripts
author Matthew Wild <mwild1@gmail.com>
date Sat, 25 Feb 2017 16:54:52 +0000
parents f65c5927ee8e
children 5e948d1392a5
comparison
equal deleted inserted replaced
2577:00cef058df8d 2578:6dbd07f9a868
1 1
2 local lfs = require "lfs";
2 local resolve_relative_path = require "core.configmanager".resolve_relative_path; 3 local resolve_relative_path = require "core.configmanager".resolve_relative_path;
3 local logger = require "util.logger".init; 4 local logger = require "util.logger".init;
4 local it = require "util.iterators"; 5 local it = require "util.iterators";
6 local set = require "util.set";
5 7
6 local definitions = module:shared("definitions"); 8 local definitions = module:shared("definitions");
7 local active_definitions = { 9 local active_definitions = {
8 ZONE = { 10 ZONE = {
9 -- Default zone that includes all local hosts 11 -- Default zone that includes all local hosts
547 script_path = script_path:match("^module:(.+)$"); 549 script_path = script_path:match("^module:(.+)$");
548 end 550 end
549 return resolve_relative_path(relative_to, script_path); 551 return resolve_relative_path(relative_to, script_path);
550 end 552 end
551 553
554 -- [filename] = { last_modified = ..., events_hooked = { [name] = handler } }
555 local loaded_scripts = {};
556
552 function load_script(script) 557 function load_script(script)
553 script = resolve_script_path(script); 558 script = resolve_script_path(script);
554 local chain_functions, err = compile_firewall_rules(script) 559 local last_modified = (lfs.attributes(script) or {}).modification or os.time();
560 if loaded_scripts[script] then
561 if loaded_scripts[script].last_modified == last_modified then
562 return; -- Already loaded, and source file hasn't changed
563 end
564 module:log("debug", "Reloading %s", script);
565 -- Already loaded, but the source file has changed
566 -- unload it now, and we'll load the new version below
567 unload_script(script, true);
568 end
569 local chain_functions, err = compile_firewall_rules(script);
555 570
556 if not chain_functions then 571 if not chain_functions then
557 module:log("error", "Error compiling %s: %s", script, err or "unknown error"); 572 module:log("error", "Error compiling %s: %s", script, err or "unknown error");
558 else 573 return;
559 for chain, handler_code in pairs(chain_functions) do 574 end
560 local new_handler, err = compile_handler(handler_code, "mod_firewall::"..chain); 575
561 if not new_handler then 576 -- Loop through the chains in the script, and for each chain attach the compiled code to the
562 module:log("error", "Compilation error for %s: %s", script, err); 577 -- relevant events, keeping track in events_hooked so we can cleanly unload later
563 else 578 local events_hooked = {};
564 local chain_definition = chains[chain]; 579 for chain, handler_code in pairs(chain_functions) do
565 if chain_definition and chain_definition.type == "event" then 580 local new_handler, err = compile_handler(handler_code, "mod_firewall::"..chain);
566 local handler = new_handler(chain_definition.pass_return); 581 if not new_handler then
567 for _, event_name in ipairs(chain_definition) do 582 module:log("error", "Compilation error for %s: %s", script, err);
568 module:hook(event_name, handler, chain_definition.priority); 583 else
569 end 584 local chain_definition = chains[chain];
570 elseif not chain:sub(1, 5) == "user/" then 585 if chain_definition and chain_definition.type == "event" then
571 module:log("warn", "Unknown chain %q", chain); 586 local handler = new_handler(chain_definition.pass_return);
572 end 587 for _, event_name in ipairs(chain_definition) do
573 module:hook("firewall/chains/"..chain, new_handler(false)); 588 events_hooked[event_name] = handler;
574 end 589 module:hook(event_name, handler, chain_definition.priority);
575 end 590 end
591 elseif not chain:sub(1, 5) == "user/" then
592 module:log("warn", "Unknown chain %q", chain);
593 end
594 local event_name, handler = "firewall/chains/"..chain, new_handler(false);
595 events_hooked[event_name] = handler;
596 module:hook(event_name, handler);
597 end
598 end
599 loaded_scripts[script] = { last_modified = last_modified, events_hooked = events_hooked };
600 module:log("debug", "Loaded %s", script);
601 end
602
603 function unload_script(script, is_reload)
604 script = resolve_script_path(script);
605 local script_info = loaded_scripts[script];
606 if not script_info then
607 return; -- Script not loaded
608 end
609 local events_hooked = script_info.events_hooked;
610 for event_name, event_handler in pairs(events_hooked) do
611 module:unhook(event_name, event_handler);
612 events_hooked[event_name] = nil;
613 end
614 loaded_scripts[script] = nil;
615 if not is_reload then
616 module:log("debug", "Unloaded %s", script);
617 end
618 end
619
620 -- Given a set of scripts (e.g. from config) figure out which ones need to
621 -- be loaded, which are already loaded but need unloading, and which to reload
622 function load_unload_scripts(script_list)
623 local wanted_scripts = script_list / resolve_script_path;
624 local currently_loaded = set.new(it.to_array(it.keys(loaded_scripts)));
625 local scripts_to_unload = currently_loaded - wanted_scripts;
626 for script in wanted_scripts do
627 -- If the script is already loaded, this is fine - it will
628 -- reload the script for us if the file has changed
629 load_script(script);
630 end
631 for script in scripts_to_unload do
632 unload_script(script);
576 end 633 end
577 end 634 end
578 635
579 function module.load() 636 function module.load()
580 if not prosody.arg then return end -- Don't run in prosodyctl 637 if not prosody.arg then return end -- Don't run in prosodyctl
581 active_definitions = {};
582 local firewall_scripts = module:get_option_set("firewall_scripts", {}); 638 local firewall_scripts = module:get_option_set("firewall_scripts", {});
583 for script in firewall_scripts do 639 load_unload_scripts(firewall_scripts);
584 load_script(script);
585 end
586 -- Replace contents of definitions table (shared) with active definitions 640 -- Replace contents of definitions table (shared) with active definitions
587 for k in it.keys(definitions) do definitions[k] = nil; end 641 for k in it.keys(definitions) do definitions[k] = nil; end
588 for k,v in pairs(active_definitions) do definitions[k] = v; end 642 for k,v in pairs(active_definitions) do definitions[k] = v; end
589 end 643 end
644
645 function module.save()
646 return { active_definitions = active_definitions, loaded_scripts = loaded_scripts };
647 end
648
649 function module.restore(state)
650 active_definitions = state.active_definitions;
651 loaded_scripts = state.loaded_scripts;
652 end
653
654 module:hook_global("config-reloaded", function ()
655 load_unload_scripts(module:get_option_set("firewall_scripts", {}));
656 end);
590 657
591 function module.command(arg) 658 function module.command(arg)
592 if not arg[1] or arg[1] == "--help" then 659 if not arg[1] or arg[1] == "--help" then
593 require"util.prosodyctl".show_usage([[mod_firewall <firewall.pfw>]], [[Compile files with firewall rules to Lua code]]); 660 require"util.prosodyctl".show_usage([[mod_firewall <firewall.pfw>]], [[Compile files with firewall rules to Lua code]]);
594 return 1; 661 return 1;