This repository has been archived by the owner on Jul 11, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathclient.lua
1571 lines (1396 loc) · 52 KB
/
client.lua
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--------------------------------------------------------------------------
-- DNS client.
--
-- Works with OpenResty only. Requires the [`lua-resty-dns`](https://github.com/openresty/lua-resty-dns) module.
--
-- _NOTES_:
--
-- 1. parsing the config files upon initialization uses blocking i/o, so use with
-- care. See `init` for details.
-- 2. All returned records are directly from the cache. _So do not modify them!_
-- If you need to, copy them first.
-- 3. TTL for records is the TTL returned by the server at the time of fetching
-- and won't be updated while the client serves the records from its cache.
-- 4. resolving IPv4 (A-type) and IPv6 (AAAA-type) addresses is explicitly supported. If
-- the hostname to be resolved is a valid IP address, it will be cached with a ttl of
-- 10 years. So the user doesn't have to check for ip adresses.
--
-- @copyright 2016-2017 Kong Inc.
-- @author Thijs Schreijer
-- @license Apache 2.0
local _
local utils = require("resty.dns.utils")
local fileexists = require("pl.path").exists
local semaphore = require("ngx.semaphore").new
local lrucache = require("resty.lrucache")
local resolver = require("resty.dns.resolver")
local deepcopy = require("pl.tablex").deepcopy
local time = ngx.now
local log = ngx.log
local ERR = ngx.ERR
local WARN = ngx.WARN
local DEBUG = ngx.DEBUG
local PREFIX = "[dns-client] "
local timer_at = ngx.timer.at
local get_phase = ngx.get_phase
local math_min = math.min
local math_max = math.max
local math_fmod = math.fmod
local math_random = math.random
local table_remove = table.remove
local table_insert = table.insert
local table_concat = table.concat
local string_lower = string.lower
local EMPTY = setmetatable({},
{__newindex = function() error("The 'EMPTY' table is read-only") end})
-- resolver options
local config
local defined_hosts -- hash table to lookup names originating from the hosts file
local emptyTtl -- ttl (in seconds) for empty and 'name error' (3) errors
local badTtl -- ttl (in seconds) for a other dns error results
local staleTtl -- ttl (in seconds) to serve stale data (while new lookup is in progress)
local validTtl -- ttl (in seconds) to use to override ttl of any valid answer
local cacheSize -- size of the lru cache
local noSynchronisation
local orderValids = {"LAST", "SRV", "A", "AAAA", "CNAME"} -- default order to query
local typeOrder -- array with order of types to try
local clientErrors = { -- client specific errors
[100] = "cache only lookup failed",
[101] = "empty record received",
}
for _,v in ipairs(orderValids) do orderValids[v:upper()] = v end
-- create module table
local _M = {}
-- copy resty based constants for record types
for k,v in pairs(resolver) do
if type(k) == "string" and k:sub(1,5) == "TYPE_" then
_M[k] = v
end
end
-- insert our own special value for "last success"
_M.TYPE_LAST = -1
-- ==============================================
-- Debugging aid
-- ==============================================
-- to be enabled manually by doing a replace-all on the
-- long comment start.
--[[
local json = require("cjson").encode
local function fquery(item)
return (tostring(item):gsub("table: ", "query="))
end
local function frecord(record)
if type(record) ~= "table" then
return tostring(record)
end
return (tostring(record):gsub("table: ", "record=")) .. " " .. json(record)
end
--]]
-- ==============================================
-- In memory DNS cache
-- ==============================================
--- Caching.
-- The cache will not update the `ttl` field. So every time the same record
-- is served, the ttl will be the same. But the cache will insert extra fields
-- on the top-level; `touch` (timestamp of last access), `expire` (expiry time
-- based on `ttl`), and `expired` (boolean indicating it expired/is stale)
-- @section caching
-- hostname lru-cache indexed by "recordtype:hostname" returning address list.
-- short names are indexed by "recordtype:short:hostname"
-- Result is a list with entries.
-- Keys only by "hostname" only contain the last succesfull lookup type
-- for this name, see `resolve` function.
local dnscache
-- lookup a single entry in the cache.
-- @param qname name to lookup
-- @param qtype type number, any of the TYPE_xxx constants
-- @return cached record or nil
local cachelookup = function(qname, qtype)
local now = time()
local key = qtype..":"..qname
local cached = dnscache:get(key)
if cached then
cached.touch = now
if (cached.expire < now) then
cached.expired = true
--[[
log(DEBUG, PREFIX, "cache get (stale): ", key, " ", frecord(cached))
else
log(DEBUG, PREFIX, "cache get: ", key, " ", frecord(cached))
--]]
end
--[[
else
log(DEBUG, PREFIX, "cache get (miss): ", key)
--]]
end
return cached
end
-- inserts an entry in the cache.
-- @param entry the dns record list to store (may also be an error entry)
-- @param qname the name under which to store the record (optional for records, not for errors)
-- @param qtype the query type for which to store the record (optional for records, not for errors)
-- @return nothing
local cacheinsert = function(entry, qname, qtype)
local key, lru_ttl
local now = time()
local e1 = entry[1]
if not entry.expire then
-- new record not seen before
local ttl
if e1 then
-- an actual, non-empty, record
key = (qtype or e1.type) .. ":" .. (qname or e1.name)
ttl = validTtl or math.huge
for i = 1, #entry do
local record = entry[i]
if validTtl then
-- force configured ttl
record.ttl = validTtl
else
-- determine minimum ttl of all answer records
ttl = math_min(ttl, record.ttl)
end
-- update IPv6 address format to include square brackets
if record.type == _M.TYPE_AAAA then
record.address = utils.parseHostname(record.address)
elseif record.type == _M.TYPE_SRV then -- SRV can also contain IPv6
record.target = utils.parseHostname(record.target)
end
end
elseif entry.errcode and entry.errcode ~= 3 then
-- an error, but no 'name error' (3)
if (cachelookup(qname, qtype) or EMPTY)[1] then
-- we still have a stale record with data, so we're not replacing that
--[[
log(DEBUG, PREFIX, "cache set (skip on name error): ", key, " ", frecord(entry))
--]]
return
end
ttl = badTtl
key = qtype..":"..qname
elseif entry.errcode == 3 then
-- a 'name error' (3)
ttl = emptyTtl
key = qtype..":"..qname
else
-- empty record
if (cachelookup(qname, qtype) or EMPTY)[1] then
-- we still have a stale record with data, so we're not replacing that
--[[
log(DEBUG, PREFIX, "cache set (skip on empty): ", key, " ", frecord(entry))
--]]
return
end
ttl = emptyTtl
key = qtype..":"..qname
end
-- set expire time
entry.touch = now
entry.ttl = ttl
entry.expire = now + ttl
entry.expired = false
lru_ttl = ttl + staleTtl
--[[
log(DEBUG, PREFIX, "cache set (new): ", key, " ", frecord(entry))
--]]
else
-- an existing record reinserted (under a shortname for example)
-- must calculate remaining ttl, cannot get it from lrucache
key = (qtype or e1.type) .. ":" .. (qname or e1.name)
lru_ttl = entry.expire - now + staleTtl
--[[
log(DEBUG, PREFIX, "cache set (existing): ", key, " ", frecord(entry))
--]]
end
if lru_ttl <= 0 then
-- item is already expired, so we do not add it
dnscache:delete(key)
--[[
log(DEBUG, PREFIX, "cache set (delete on expired): ", key, " ", frecord(entry))
--]]
return
end
dnscache:set(key, entry, lru_ttl)
end
-- Lookup a shortname in the cache.
-- @param qname the name to lookup
-- @param qtype (optional) if not given a non-type specific query is done
-- @return same as cachelookup
local function cacheShortLookup(qname, qtype)
return cachelookup("short:" .. qname, qtype or "none")
end
-- Inserts a shortname in the cache.
-- @param qname the name to lookup
-- @param qtype (optional) if not given a non-type specific insertion is done
-- @return nothing
local function cacheShortInsert(entry, qname, qtype)
return cacheinsert(entry, "short:" .. qname, qtype or "none")
end
-- Lookup the last succesful query type.
-- @param qname name to resolve
-- @return query/record type constant, or ˋnilˋ if not found
local function cachegetsuccess(qname)
return dnscache:get(qname)
end
-- Sets the last succesful query type.
-- Only if the type provided is in the list of types to try.
-- @param qname name resolved
-- @param qtype query/record type to set, or ˋnilˋ to clear
-- @return `true` if set, or `false` if not
local function cachesetsuccess(qname, qtype)
-- Test whether the qtype value is in our search/order list
local validType = false
for _, t in ipairs(typeOrder) do
if t == qtype then
validType = true
break
end
end
if not validType then
-- the qtype is not in the list, so we're not setting it as the
-- success type
--[[
log(DEBUG, PREFIX, "cache set success (skip on bad type): ", qname, ", ", qtype)
--]]
return false
end
dnscache:set(qname, qtype)
--[[
log(DEBUG, PREFIX, "cache set success: ", qname, " = ", qtype)
--]]
return true
end
-- =====================================================
-- Try/status list for recursion checks and logging
-- =====================================================
local msg_mt = {
__tostring = function(self)
return table_concat(self, "/")
end
}
local try_list_mt = {
__tostring = function(self)
local l, i = {}, 0
for _, entry in ipairs(self) do
l[i] = '","'
l[i+1] = entry.qname
l[i+2] = ":"
l[i+3] = entry.qtype or "(na)"
local m = tostring(entry.msg):gsub('"',"'")
if m == "" then
i = i + 4
else
l[i+4] = " - "
l[i+5] = m
i = i + 6
end
end
-- concatenate result and encode as json array
return '["' .. table_concat(l) .. '"]'
end
}
-- adds a try to a list of tries.
-- The list keeps track of all queries tried so far. The array part lists the
-- order of attempts, whilst the `<qname>:<qtype>` key contains the index of that try.
-- @param self (optional) the list to add to, if omitted a new one will be created and returned
-- @param qname name being looked up
-- @param qtype query type being done
-- @param status (optional) message to be recorded
-- @return the list
local function try_add(self, qname, qtype, status)
self = self or setmetatable({}, try_list_mt)
local key = tostring(qname) .. ":" .. tostring(qtype)
local idx = #self + 1
self[idx] = {
qname = qname,
qtype = qtype,
msg = setmetatable({ status }, msg_mt),
}
self[key] = idx
return self
end
-- adds a status to the last try.
-- @param self the try_list to add to
-- @param status string with current status, added to the list for the current try
-- @return the try_list
local function try_status(self, status)
local status_list = self[#self].msg
status_list[#status_list + 1] = status
return self
end
-- ==============================================
-- Main DNS functions for lookup
-- ==============================================
--- Resolving.
-- When resolving names, queries will be synchronized, such that only a single
-- query will be sent. If stale data is available, the request will return
-- stale data immediately, whilst continuing to resolve the name in the
-- background.
--
-- The `dnsCacheOnly` parameter found with `resolve` and `toip` can be used in
-- contexts where the co-socket api is unavailable. When the flag is set
-- only cached data is returned, but it will never use blocking io.
-- @section resolving
local poolMaxWait
local poolMaxRetry
--- Initialize the client. Can be called multiple times. When called again it
-- will clear the cache.
-- @param options Same table as the [OpenResty dns resolver](https://github.com/openresty/lua-resty-dns),
-- with some extra fields explained in the example below.
-- @return `true` on success, `nil+error`, or throw an error on bad input
-- @usage -- config files to parse
-- -- `hosts` and `resolvConf` can both be a filename, or a table with file-contents
-- -- The contents of the `hosts` file will be inserted in the cache.
-- -- From `resolv.conf` the `nameserver`, `search`, `ndots`, `attempts` and `timeout` values will be used.
-- local hosts = {} -- initialize without any blocking i/o
-- local resolvConf = {} -- initialize without any blocking i/o
--
-- -- when getting nameservers from `resolv.conf`, get ipv6 servers?
-- local enable_ipv6 = false
--
-- -- Order in which to try different dns record types when resolving
-- -- 'last'; will try the last previously successful type for a hostname.
-- local order = { "last", "SRV", "A", "AAAA", "CNAME" }
--
-- -- Stale ttl for how long a stale record will be served from the cache
-- -- while a background lookup is in progress.
-- local staleTtl = 4.0 -- in seconds (can have fractions)
--
-- -- Cache ttl for empty and 'name error' (3) responses
-- local emptyTtl = 30.0 -- in seconds (can have fractions)
--
-- -- Cache ttl for other error responses
-- local badTtl = 1.0 -- in seconds (can have fractions)
--
-- -- Overriding ttl for valid queries, if given
-- local validTtl = nil -- in seconds (can have fractions)
--
-- -- `ndots`, same as the `resolv.conf` option, if not given it is taken from
-- -- `resolv.conf` or otherwise set to 1
-- local ndots = 1
--
-- -- `no_random`, if set disables randomly picking the first nameserver, if not
-- -- given it is taken from `resolv.conf` option `rotate` (inverted).
-- -- Defaults to `true`.
-- local no_random = true
--
-- -- `search`, same as the `resolv.conf` option, if not given it is taken from
-- -- `resolv.conf`, or set to the `domain` option, or no search is performed
-- local search = {
-- "mydomain.com",
-- "site.domain.org",
-- }
--
-- -- Disables synchronization between queries, resulting in each lookup for the
-- -- same name being executed in it's own query to the nameservers. The default
-- -- (`false`) will synchronize multiple queries for the same name to a single
-- -- query to the nameserver.
-- noSynchronisation = false
--
-- assert(client.init({
-- hosts = hosts,
-- resolvConf = resolvConf,
-- ndots = ndots,
-- no_random = no_random,
-- search = search,
-- order = order,
-- badTtl = badTtl,
-- emptyTtl = emptTtl,
-- staleTtl = staleTtl,
-- validTtl = validTtl,
-- enable_ipv6 = enable_ipv6,
-- noSynchronisation = noSynchronisation,
-- })
-- )
_M.init = function(options)
log(DEBUG, PREFIX, "(re)configuring dns client")
local resolv, hosts, err
options = options or {}
staleTtl = options.staleTtl or 4
log(DEBUG, PREFIX, "staleTtl = ", staleTtl)
cacheSize = options.cacheSize or 10000 -- default set here to be able to reset the cache
noSynchronisation = options.noSynchronisation
log(DEBUG, PREFIX, "noSynchronisation = ", tostring(noSynchronisation))
dnscache = lrucache.new(cacheSize) -- clear cache on (re)initialization
defined_hosts = {} -- reset hosts hash table
local order = options.order or orderValids
typeOrder = {} -- clear existing upvalue
local ip_preference
for i,v in ipairs(order) do
local t = v:upper()
if not ip_preference and (t == "A" or t == "AAAA") then
-- the first one up in the list is the IP type (v4 or v6) that we
-- prefer
ip_preference = t
end
assert(orderValids[t], "Invalid dns record type in order array; "..tostring(v))
typeOrder[i] = _M["TYPE_"..t]
end
assert(#typeOrder > 0, "Invalid order list; cannot be empty")
log(DEBUG, PREFIX, "query order = ", table_concat(order,", "))
-- Deal with the `hosts` file
local hostsfile = options.hosts or utils.DEFAULT_HOSTS
if ((type(hostsfile) == "string") and (fileexists(hostsfile)) or
(type(hostsfile) == "table")) then
hosts, err = utils.parseHosts(hostsfile) -- results will be all lowercase!
if not hosts then return hosts, err end
else
log(WARN, PREFIX, "Hosts file not found: "..tostring(hostsfile))
hosts = {}
end
-- treat `localhost` special, by always defining it, RFC 6761: Section 6.3.3
if not hosts.localhost then
hosts.localhost = {
ipv4 = "127.0.0.1",
ipv6 = "[::1]",
}
end
-- Populate the DNS cache with the hosts (and aliasses) from the hosts file.
local ttl = 10*365*24*60*60 -- use ttl of 10 years for hostfile entries
for name, address in pairs(hosts) do
name = string_lower(name)
if address.ipv4 then
cacheinsert({{ -- NOTE: nested list! cache is a list of lists
name = name,
address = address.ipv4,
type = _M.TYPE_A,
class = 1,
ttl = ttl,
}})
defined_hosts[name..":".._M.TYPE_A] = true
-- cache is empty so far, so no need to check for the ip_preference
-- field here, just set ipv4 as success-type.
cachesetsuccess(name, _M.TYPE_A)
log(DEBUG, PREFIX, "adding A-record from 'hosts' file: ",name, " = ", address.ipv4)
end
if address.ipv6 then
cacheinsert({{ -- NOTE: nested list! cache is a list of lists
name = name,
address = address.ipv6,
type = _M.TYPE_AAAA,
class = 1,
ttl = ttl,
}})
defined_hosts[name..":".._M.TYPE_AAAA] = true
-- do not overwrite the A success-type unless AAAA is preferred
if ip_preference == "AAAA" or not cachegetsuccess(name) then
cachesetsuccess(name, _M.TYPE_AAAA)
end
log(DEBUG, PREFIX, "adding AAAA-record from 'hosts' file: ",name, " = ", address.ipv6)
end
end
-- see: https://github.com/Kong/kong/issues/7444
-- since the validTtl affects ttl of caching entries,
-- only set it after hosts entries are inserted
-- so that the 10 years of TTL for hosts file actually takes effect.
validTtl = options.validTtl
log(DEBUG, PREFIX, "validTtl = ", tostring(validTtl))
-- Deal with the `resolv.conf` file
local resolvconffile = options.resolvConf or utils.DEFAULT_RESOLV_CONF
if ((type(resolvconffile) == "string") and (fileexists(resolvconffile)) or
(type(resolvconffile) == "table")) then
resolv, err = utils.applyEnv(utils.parseResolvConf(resolvconffile))
if not resolv then return resolv, err end
else
log(WARN, PREFIX, "Resolv.conf file not found: "..tostring(resolvconffile))
resolv = {}
end
if not resolv.options then resolv.options = {} end
if #(options.nameservers or {}) == 0 and resolv.nameserver then
options.nameservers = {}
-- some systems support port numbers in nameserver entries, so must parse those
for _, address in ipairs(resolv.nameserver) do
local ip, port, t = utils.parseHostname(address)
if t == "ipv6" and not options.enable_ipv6 then
-- should not add this one
log(DEBUG, PREFIX, "skipping IPv6 nameserver ", port and (ip..":"..port) or ip)
elseif t == "ipv6" and ip:find([[%]], nil, true) then
-- ipv6 with a scope
log(DEBUG, PREFIX, "skipping IPv6 nameserver (scope not supported) ", port and (ip..":"..port) or ip)
else
if port then
options.nameservers[#options.nameservers + 1] = { ip, port }
else
options.nameservers[#options.nameservers + 1] = ip
end
end
end
end
options.nameservers = options.nameservers or {}
if #options.nameservers == 0 then
log(WARN, PREFIX, "Invalid configuration, no valid nameservers found")
else
for _, r in ipairs(options.nameservers) do
log(DEBUG, PREFIX, "nameserver ", type(r) == "table" and (r[1]..":"..r[2]) or r)
end
end
options.retrans = options.retrans or resolv.options.attempts or 5 -- 5 is openresty default
log(DEBUG, PREFIX, "attempts = ", options.retrans)
if options.no_random == nil then
options.no_random = not resolv.options.rotate
else
options.no_random = not not options.no_random -- force to boolean
end
log(DEBUG, PREFIX, "no_random = ", options.no_random)
if not options.timeout then
if resolv.options.timeout then
options.timeout = resolv.options.timeout * 1000
else
options.timeout = 2000 -- 2000 is openresty default
end
end
log(DEBUG, PREFIX, "timeout = ", options.timeout, " ms")
-- setup the search order
options.ndots = options.ndots or resolv.options.ndots or 1
log(DEBUG, PREFIX, "ndots = ", options.ndots)
options.search = options.search or resolv.search or { resolv.domain }
log(DEBUG, PREFIX, "search = ", table_concat(options.search,", "))
-- other options
badTtl = options.badTtl or 1
log(DEBUG, PREFIX, "badTtl = ", badTtl, " s")
emptyTtl = options.emptyTtl or 30
log(DEBUG, PREFIX, "emptyTtl = ", emptyTtl, " s")
-- options.no_recurse = -- not touching this one for now
config = options -- store it in our module level global
poolMaxRetry = 1 -- do one retry, dns resolver is already doing 'retrans' number of retries on top
poolMaxWait = options.timeout / 1000 * options.retrans -- default is to wait for the dns resolver to hit its timeouts
return true
end
-- Removes non-requested results, updates the cache.
-- Parameter `answers` is updated in-place.
-- @return `true`
local function parseAnswer(qname, qtype, answers, try_list)
-- check the answers and store them in the cache
-- eg. A, AAAA, SRV records may be accompanied by CNAME records
-- store them all, leaving only the requested type in so we can return that set
local others = {}
-- remove last '.' from FQDNs as the answer does not contain it
local check_qname do
if qname:sub(-1, -1) == "." then
check_qname = qname:sub(1, -2) -- FQDN, drop the last dot
else
check_qname = qname
end
end
for i = #answers, 1, -1 do -- we're deleting entries, so reverse the traversal
local answer = answers[i]
-- normalize casing
answer.name = string_lower(answer.name)
if (answer.type ~= qtype) or (answer.name ~= check_qname) then
local key = answer.type..":"..answer.name
try_status(try_list, key .. " removed")
local lst = others[key]
if not lst then
lst = {}
others[key] = lst
end
table_insert(lst, 1, answer) -- pos 1: preserve order
table_remove(answers, i)
end
end
if next(others) then
for _, lst in pairs(others) do
cacheinsert(lst)
-- set success-type, only if not set (this is only a 'by-product')
if not cachegetsuccess(lst[1].name) then
cachesetsuccess(lst[1].name, lst[1].type)
end
end
end
-- now insert actual target record in cache
cacheinsert(answers, qname, qtype)
return true
end
-- executes 1 individual query.
-- This query will not be synchronized, every call will be 1 query.
-- @param qname the name to query for
-- @param r_opts a table with the query options
-- @param try_list the try_list object to add to
-- @return `result + nil + try_list`, or `nil + err + try_list` in case of errors
local function individualQuery(qname, r_opts, try_list)
local r, err = resolver:new(config)
if not r then
return r, "failed to create a resolver: " .. err, try_list
end
try_status(try_list, "querying")
local result
result, err = r:query(qname, r_opts)
if not result then
return result, err, try_list
end
parseAnswer(qname, r_opts.qtype, result, try_list)
return result, nil, try_list
end
local queue = setmetatable({}, {__mode = "v"})
-- to be called as a timer-callback, performs a query and returns the results
-- in the `item` table.
local function executeQuery(premature, item)
if premature then return end
local r, err = resolver:new(config)
if not r then
item.result, item.err = r, "failed to create a resolver: " .. err
else
--[[
log(DEBUG, PREFIX, "Query executing: ", item.qname, ":", item.r_opts.qtype, " ", fquery(item))
--]]
try_status(item.try_list, "querying")
item.result, item.err = r:query(item.qname, item.r_opts)
if item.result then
--[[
log(DEBUG, PREFIX, "Query answer: ", item.qname, ":", item.r_opts.qtype, " ", fquery(item),
" ", frecord(item.result))
--]]
parseAnswer(item.qname, item.r_opts.qtype, item.result, item.try_list)
--[[
log(DEBUG, PREFIX, "Query parsed answer: ", item.qname, ":", item.r_opts.qtype, " ", fquery(item),
" ", frecord(item.result))
else
log(DEBUG, PREFIX, "Query error: ", item.qname, ":", item.r_opts.qtype, " err=", tostring(err))
--]]
end
end
-- query done, but by now many others might be waiting for our result.
-- 1) stop new ones from adding to our lock/semaphore
queue[item.key] = nil
-- 2) release all waiting threads
item.semaphore:post(math_max(item.semaphore:count() * -1, 1))
item.semaphore = nil
end
-- schedules an async query.
-- This will be synchronized, so multiple calls (sync or async) might result in 1 query.
-- @param qname the name to query for
-- @param r_opts a table with the query options
-- @param try_list the try_list object to add to
-- @return `item` table which will receive the `result` and/or `err` fields, and a
-- `semaphore` field that can be used to wait for completion (once complete
-- the `semaphore` field will be removed). Upon error it returns `nil+error`.
local function asyncQuery(qname, r_opts, try_list)
local key = qname..":"..r_opts.qtype
local item = queue[key]
if item then
--[[
log(DEBUG, PREFIX, "Query async (exists): ", key, " ", fquery(item))
--]]
try_status(try_list, "in progress (async)")
return item -- already in progress, return existing query
end
item = {
key = key,
semaphore = semaphore(),
qname = qname,
r_opts = deepcopy(r_opts),
try_list = try_list,
}
queue[key] = item
local ok, err = timer_at(0, executeQuery, item)
if not ok then
queue[key] = nil
log(ERR, PREFIX, "Failed to create a timer: ", err)
return nil, "asyncQuery failed to create timer: "..err
end
--[[
log(DEBUG, PREFIX, "Query async (scheduled): ", key, " ", fquery(item))
--]]
try_status(try_list, "scheduled")
return item
end
-- schedules a sync query.
-- This will be synchronized, so multiple calls (sync or async) might result in 1 query.
-- The `poolMaxWait` is how long a thread waits for another to complete the query.
-- The `poolMaxRetry` is how often we wait for another query to complete.
-- The maximum delay would be `poolMaxWait * poolMaxRetry`.
-- @param qname the name to query for
-- @param r_opts a table with the query options
-- @param try_list the try_list object to add to
-- @return `result + nil + try_list`, or `nil + err + try_list` in case of errors
local function syncQuery(qname, r_opts, try_list, count)
local key = qname..":"..r_opts.qtype
local item = queue[key]
count = count or 1
-- if nothing is in progress, we start a new async query
if not item then
local err
item, err = asyncQuery(qname, r_opts, try_list)
--[[
log(DEBUG, PREFIX, "Query sync (new): ", key, " ", fquery(item)," count=", count)
--]]
if not item then
return item, err, try_list
end
else
--[[
log(DEBUG, PREFIX, "Query sync (exists): ", key, " ", fquery(item)," count=", count)
--]]
try_status(try_list, "in progress (sync)")
end
local supported_semaphore_wait_phases = {
rewrite = true,
access = true,
content = true,
timer = true,
ssl_cert = true,
ssl_session_fetch = true,
}
local ngx_phase = get_phase()
if not supported_semaphore_wait_phases[ngx_phase] then
-- phase not supported by `semaphore:wait`
-- return existing query (item)
--
-- this will avoid:
-- "dns lookup pool exceeded retries" (second try and subsequent retries)
-- "API disabled in the context of init_worker_by_lua" (first try)
return item, nil, try_list
end
-- block and wait for the async query to complete
local ok, err = item.semaphore:wait(poolMaxWait)
if ok and item.result then
-- we were released, and have a query result from the
-- other thread, so all is well, return it
--[[
log(DEBUG, PREFIX, "Query sync result: ", key, " ", fquery(item),
" result: ", json({ result = item.result, err = item.err}))
--]]
return item.result, item.err, try_list
end
-- there was an error, either a semaphore timeout, or a lookup error
-- go retry
try_status(try_list, "try "..count.." error: "..(item.err or err or "unknown"))
if count > poolMaxRetry then
--[[
log(DEBUG, PREFIX, "Query sync (fail): ", key, " ", fquery(item)," retries exceeded. count=", count)
--]]
return nil, "dns lookup pool exceeded retries (" ..
tostring(poolMaxRetry) .. "): "..tostring(item.err or err),
try_list
end
-- don't block on the same thread again, so remove it from the queue
if queue[key] == item then queue[key] = nil end
--[[
log(DEBUG, PREFIX, "Query sync (fail): ", key, " ", fquery(item)," retrying. count=", count)
--]]
return syncQuery(qname, r_opts, try_list, count + 1)
end
-- will lookup a name in the cache, or alternatively query the nameservers.
-- If nothing is in the cache, a synchronous query is performewd. If the cache
-- contains stale data, that stale data is returned while an asynchronous
-- lookup is started in the background.
-- @param qname the name to look for
-- @param r_opts a table with the query options
-- @param dnsCacheOnly if true, no active lookup is done when there is no (stale)
-- data. In that case an error is returned (as a dns server failure table).
-- @param try_list the try_list object to add to
-- @return `entry + nil + try_list`, or `nil + err + try_list`
local function lookup(qname, r_opts, dnsCacheOnly, try_list)
local entry = cachelookup(qname, r_opts.qtype)
if not entry then
--not found in cache
if dnsCacheOnly then
-- we can't do a lookup, so return an error
--[[
log(DEBUG, PREFIX, "Lookup, cache only failure: ", qname, " = ", r_opts.qtype)
--]]
try_list = try_add(try_list, qname, r_opts.qtype, "cache only lookup failed")
return {
errcode = 100,
errstr = clientErrors[100]
}, nil, try_list
end
-- perform a sync lookup, as we have no stale data to fall back to
try_list = try_add(try_list, qname, r_opts.qtype, "cache-miss")
if noSynchronisation then
return individualQuery(qname, r_opts, try_list)
end
return syncQuery(qname, r_opts, try_list)
end
try_list = try_add(try_list, qname, r_opts.qtype, "cache-hit")
if entry.expired then
-- the cached record is stale but usable, so we do a refresh query in the background
try_status(try_list, "stale")
asyncQuery(qname, r_opts, try_list)
end
return entry, nil, try_list
end
-- checks the query to be a valid IPv6. Inserts it in the cache or inserts
-- an error if it is invalid
-- @param qname the IPv6 address to check
-- @param qtype query type performed, any of the `TYPE_xx` constants
-- @param try_list the try_list object to add to
-- @return record as cached, nil, try_list
local function check_ipv6(qname, qtype, try_list)
try_list = try_add(try_list, qname, qtype, "IPv6")
local record = cachelookup(qname, qtype)
if record then
try_status(try_list, "cached")
return record, nil, try_list
end
local check = qname:match("^%[(.+)%]$") -- grab contents of "[ ]"
if not check then
-- no square brackets found
check = qname
end
if check:sub(1,1) == ":" then check = "0"..check end
if check:sub(-1,-1) == ":" then check = check.."0" end
if check:find("::") then
-- expand double colon
local _, count = check:gsub(":","")
local ins = ":"..string.rep("0:", 8 - count)
check = check:gsub("::", ins, 1) -- replace only 1 occurence!
end
if qtype == _M.TYPE_AAAA and
check:match("^%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?:%x%x?%x?%x?$") then
try_status(try_list, "validated")
record = {{
address = qname,
type = _M.TYPE_AAAA,
class = 1,
name = qname,
ttl = 10 * 365 * 24 * 60 * 60 -- TTL = 10 years
}}
cachesetsuccess(qname, _M.TYPE_AAAA)
else
-- not a valid IPv6 address, or a bad type (non ipv6)
-- return a "server error"
try_status(try_list, "bad IPv6")
record = {
errcode = 3,
errstr = "name error",
}
end
cacheinsert(record, qname, qtype)
return record, nil, try_list
end
-- checks the query to be a valid IPv4. Inserts it in the cache or inserts
-- an error if it is invalid
-- @param qname the IPv4 address to check
-- @param qtype query type performed, any of the `TYPE_xx` constants
-- @param try_list the try_list object to add to
-- @return record as cached, nil, try_list
local function check_ipv4(qname, qtype, try_list)
try_list = try_add(try_list, qname, qtype, "IPv4")
local record = cachelookup(qname, qtype)
if record then
try_status(try_list, "cached")
return record, nil, try_list
end
if qtype == _M.TYPE_A then
try_status(try_list, "validated")
record = {{
address = qname,
type = _M.TYPE_A,
class = 1,
name = qname,