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
200
201
202
203
204
205
206
207
208
|
local https = require('ssl.https')
local ltn12 = require('ltn12')
local lfs = require('lfs')
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_no_ta_interval = 600
local rz_cur_interval = rz_default_interval
local rz_interval_randomizator_limit = 10
local rz_interval_threshold = 5
local rz_interval_min = 3600
local prefill = {
}
-- Fetch over HTTPS with peert cert checked
local function https_fetch(url, ca_file)
assert(string.match(url, '^https://'))
assert(ca_file)
local resp = {}
local r, c = https.request{
url = url,
verify = {'peer', 'fail_if_no_peer_cert' },
cafile = ca_file,
protocol = 'tlsv1_2',
sink = ltn12.sink.table(resp),
}
if r == nil then
return r, c
end
return resp, "[prefill] "..url.." downloaded"
end
-- Write zone to a file
local function zone_write(zone, fname)
local file, errmsg = io.open(fname, 'w')
if not file then
error(string.format("[prefill] unable to open file %s (%s)",
fname, errmsg))
end
for i = 1, #zone do
local zone_chunk = zone[i]
file:write(zone_chunk)
end
file:close()
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 attrs = lfs.attributes(fname)
if attrs then
local age = os.time() - attrs.modification
return math.max(
rz_cur_interval - age,
0)
else
return 0 -- file does not exist, download now
end
end
local function download(url, fname)
log("[prefill] downloading root zone...")
local rzone, err = https_fetch(url, rz_ca_file)
if rzone == nil then
error(string.format("[prefill] fetch of `%s` failed: %s", url, err))
end
log("[prefill] saving root zone...")
zone_write(rzone, fname)
end
local function import(fname)
local res = cache.zone_import(fname)
if res.code == 1 then -- no TA found, wait
error("[prefill] no trust anchor found for root zone, import aborted")
elseif res.code == 0 then
log("[prefill] root zone successfully parsed, import started")
else
error(string.format("[prefill] root zone import failed (%s)", res.msg))
end
end
local function timer()
local file_ttl = get_file_ttl(rz_local_fname)
if file_ttl > rz_interval_threshold then
log("[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_randomizator_limit)
log("[prefill] cannot download new zone (%s), "
.. "will retry root zone download in %s",
errmsg, display_delay(rz_cur_interval))
event.reschedule(rz_event_id, rz_cur_interval * sec)
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
rz_cur_interval = rz_no_ta_interval
- math.random(rz_interval_randomizator_limit)
log("[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_randomizator_limit))
log("[prefill] root zone refresh in %s",
display_delay(rz_cur_interval))
end
event.reschedule(rz_event_id, rz_cur_interval * sec)
end
function prefill.init()
math.randomseed(os.time())
end
function prefill.deinit()
if rz_event_id then
event.cancel(rz_event_id)
rz_event_id = nil
end
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('[prefill] 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
if not zone_cfg.ca_file then
error('[prefill] option ca_file must point '
.. 'to a file with CA certificate(s) in PEM format')
end
rz_ca_file = zone_cfg.ca_file
if not zone_cfg.url or not string.match(zone_cfg.url, '^https://') then
error('[prefill] option url must contain a '
.. 'https:// URL of a zone file')
else
rz_url = zone_cfg.url
end
end
function prefill.config(config)
local root_configured = false
if not config or type(config) ~= 'table' then
error('[prefill] configuration must be in table '
.. '{owner name = {per-zone config}}')
end
for owner, zone_cfg in pairs(config) do
if owner ~= '.' then
error('[prefill] 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('[prefill] this module version requires configuration '
.. 'for root zone')
end
-- ability to change intervals
prefill.deinit()
rz_event_id = event.after(0, timer)
end
return prefill
|