1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
-- SPDX-License-Identifier: GPL-3.0-or-later
local ffi = require('ffi')
local rz_url = "https://www.internic.net/domain/root.zone"
local rz_local_fname = "root.zone"
local rz_ca_file = nil
local rz_event_id = nil
local rz_default_interval = 86400
local rz_https_fail_interval = 600
local rz_import_error_interval = 600
local rz_cur_interval = rz_default_interval
local rz_interval_randomizer_limit = 10
local rz_interval_threshold = 5
local rz_interval_min = 3600
local rz_first_try = true
local prefill = {}
-- hack for circular dependency between timer() and fill_cache()
local forward_references = {}
local function stop_timer()
if rz_event_id then
event.cancel(rz_event_id)
rz_event_id = nil
end
end
local function timer()
stop_timer()
worker.bg_worker.cq:wrap(forward_references.fill_cache)
end
local function restart_timer(after)
stop_timer()
rz_event_id = event.after(after * sec, timer)
end
local function display_delay(time)
local days = math.floor(time / 86400)
local hours = math.floor((time % 86400) / 3600)
local minutes = math.floor((time % 3600) / 60)
local seconds = math.floor(time % 60)
if days > 0 then
return string.format("%d days %02d hours", days, hours)
elseif hours > 0 then
return string.format("%02d hours %02d minutes", hours, minutes)
elseif minutes > 0 then
return string.format("%02d minutes %02d seconds", minutes, seconds)
end
return string.format("%02d seconds", seconds)
end
-- returns: number of seconds the file is valid for
-- 0 indicates immediate download
local function get_file_ttl(fname)
local mtime = tonumber(ffi.C.kr_file_mtime(fname))
if mtime > 0 then
local age = os.time() - mtime
return math.max(
rz_cur_interval - age,
0)
else
return 0 -- file does not exist, download now
end
end
local function download(url, fname)
local kluautil = require('kluautil')
local file, rcode, errmsg
file, errmsg = io.open(fname, 'w')
if not file then
error(string.format("[prefil] unable to open file %s (%s)",
fname, errmsg))
end
log_info(ffi.C.LOG_GRP_PREFILL, "downloading root zone to file %s ...", fname)
rcode, errmsg = kluautil.kr_https_fetch(url, file, rz_ca_file)
if rcode == nil then
error(string.format("[prefil] fetch of `%s` failed: %s", url, errmsg))
end
file:close()
end
local function import(fname)
local ret = ffi.C.zi_zone_import({
zone_file = fname,
time_src = ffi.C.ZI_STAMP_MTIM, -- the file might be slightly older
})
if ret == 0 then
log_info(ffi.C.LOG_GRP_PREFILL, "zone successfully parsed, import started")
else
error(string.format(
"[prefil] zone import failed: %s", ffi.C.knot_strerror(ret)
))
end
end
function forward_references.fill_cache()
local file_ttl = get_file_ttl(rz_local_fname)
if file_ttl > rz_interval_threshold then
log_info(ffi.C.LOG_GRP_PREFILL, "root zone file valid for %s, reusing data from disk",
display_delay(file_ttl))
else
local ok, errmsg = pcall(download, rz_url, rz_local_fname)
if not ok then
rz_cur_interval = rz_https_fail_interval
- math.random(rz_interval_randomizer_limit)
log_info(ffi.C.LOG_GRP_PREFILL, "cannot download new zone (%s), "
.. "will retry root zone download in %s",
errmsg, display_delay(rz_cur_interval))
restart_timer(rz_cur_interval)
os.remove(rz_local_fname)
return
end
file_ttl = rz_default_interval
end
-- file is up to date, import
-- import/filter function gets executed after resolver/module
local ok, errmsg = pcall(import, rz_local_fname)
if not ok then
if rz_first_try then
rz_first_try = false
rz_cur_interval = 1
else
rz_cur_interval = rz_import_error_interval
- math.random(rz_interval_randomizer_limit)
end
log_info(ffi.C.LOG_GRP_PREFILL, "root zone import failed (%s), retry in %s",
errmsg, display_delay(rz_cur_interval))
else
-- re-download before TTL expires
rz_cur_interval = (file_ttl - rz_interval_threshold
- math.random(rz_interval_randomizer_limit))
log_info(ffi.C.LOG_GRP_PREFILL, "root zone refresh in %s",
display_delay(rz_cur_interval))
end
restart_timer(rz_cur_interval)
end
function prefill.deinit()
stop_timer()
end
-- process one item from configuration table
-- right now it supports only root zone because
-- prefill module uses global variables
local function config_zone(zone_cfg)
if zone_cfg.interval then
zone_cfg.interval = tonumber(zone_cfg.interval)
if zone_cfg.interval < rz_interval_min then
error(string.format('[prefil] refresh interval %d s is too short, '
.. 'minimal interval is %d s',
zone_cfg.interval, rz_interval_min))
end
rz_default_interval = zone_cfg.interval
rz_cur_interval = zone_cfg.interval
end
rz_ca_file = zone_cfg.ca_file
if not zone_cfg.url or not string.match(zone_cfg.url, '^https://') then
error('[prefil] option url must contain a '
.. 'https:// URL of a zone file')
else
rz_url = zone_cfg.url
end
end
function prefill.config(config)
if config == nil then return end -- e.g. just modules = { 'prefill' }
local root_configured = false
if type(config) ~= 'table' then
error('[prefil] configuration must be in table '
.. '{owner name = {per-zone config}}')
end
for owner, zone_cfg in pairs(config) do
if owner ~= '.' then
error('[prefil] only root zone can be imported '
.. 'at the moment')
else
config_zone(zone_cfg)
root_configured = true
end
end
if not root_configured then
error('[prefil] this module version requires configuration '
.. 'for root zone')
end
restart_timer(0) -- start now
end
return prefill
|