-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local std = require("std") local json = require("cjson.safe") local listener = require("recall.listener") local default_recall_config = { ip = "127.0.0.1", port = 53, timeout = 5, retries = 2, edns_buffer_size = 4096, tcp_timeout = 10, max_tcp_connections = 100, max_tcp_per_ip = 10, max_udp_size = 4096, log_level = "info", dnssec = false, dnssec_reject_bogus = false, cache = { min_ttl = 30, max_ttl = 86400, max_negative_ttl = 300, mneme = false, mneme_path = "/var/cache/recall/dns.mneme", compact_threshold = 0.5, compact_interval = 3600, }, forward_zones = {}, scripting = { enabled = false, scripts_dir = "/etc/recall/scripts", subdomains = {}, cache_ttl = 60, script_cache_ttl = 300, }, rate_limit = { enabled = true, queries_per_second = 50, burst = 100, cleanup_interval = 15, max_buckets = 50000, }, } local configure = function(srv_cfg) local cfg = std.tbl.copy(default_recall_config) return std.tbl.merge(cfg, srv_cfg) end local get_server_config = function() local config_file = os.getenv("RECALL_CONFIG_FILE") or "/etc/recall/config.json" if not std.fs.file_exists(config_file) then return nil, "no config file found" end local config = json.decode(std.fs.read_file(config_file)) if not config then return nil, "failed to read/decode config file" end return configure(config) end local get_logger = function(self) return self.__state.logger end local track_child_pid = function(self, pid, name) if not pid or pid <= 0 then return end local child_pids = self.__state.child_pids if not child_pids then child_pids = {} self.__state.child_pids = child_pids end child_pids[pid] = name or "child" end local untrack_child_pid = function(self, pid) local child_pids = self.__state.child_pids if not child_pids or not pid or pid <= 0 then return false end if not child_pids[pid] then return false end child_pids[pid] = nil return true end local drain_exited_children = function(self) while true do local pid = std.ps.waitpid(-1) if not pid or pid <= 0 then break end untrack_child_pid(self, pid) end end local wait_for_primary_exit = function(self) if not self.__state.listener_pid then return end local logger = get_logger(self) local primary_pid = self.__state.listener_pid std.ps.register_signal(15) local got_signal = false while true do local pid, err = std.ps.wait(-1) if not pid or pid <= 0 then if err and err:find("Interrupted") then got_signal = true end break end local is_primary = pid == primary_pid untrack_child_pid(self, pid) if is_primary then break end end if got_signal and next(self.__state.child_pids) then for pid, name in pairs(self.__state.child_pids) do logger:log({ msg = "forwarding SIGTERM", process = "manager", child_pid = pid, child_name = name }) std.ps.kill(pid, 15) end local deadline = os.time() + 7 while next(self.__state.child_pids) and os.time() < deadline do std.ps.sleep_ms(200) drain_exited_children(self) end for pid, name in pairs(self.__state.child_pids) do logger:log( { msg = "sending SIGKILL to straggler", process = "manager", child_pid = pid, child_name = name }, "warn" ) std.ps.kill(pid, 9) end end drain_exited_children(self) end local spawn_listener = function(self) local logger = get_logger(self) local listener_pid = std.ps.fork() if listener_pid < 0 then logger:log({ msg = "listener spawn failed", process = "manager" }, "error") return nil end if listener_pid == 0 then local lst = listener.new(self.cfg) local ok, serve_ok, serve_err = pcall(lst.serve, lst) if not ok then logger:log({ msg = "listener crashed", process = "listener", error = tostring(serve_ok) }, "error") os.exit(1) end if not serve_ok then logger:log( { msg = "listener exited with error", process = "listener", error = tostring(serve_err) }, "error" ) os.exit(1) end os.exit(0) end logger:log({ msg = "listener spawned", process = "manager", pid = listener_pid }) self.__state.listener_pid = listener_pid track_child_pid(self, listener_pid, "listener") return true end local run = function(self) if not spawn_listener(self) then os.exit(1) end wait_for_primary_exit(self) end local new = function() local cfg, err = get_server_config() if not cfg then return nil, "failed to get RECALL server config: " .. err end local logger = std.logger.new(cfg.log_level) local instance = { cfg = cfg, __state = { logger = logger, child_pids = {}, listener_pid = nil, }, run = run, spawn_listener = spawn_listener, } return instance end return { new = new }