├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── lib └── resty │ └── dns │ └── cache.lua ├── t ├── 01-sanity.t ├── 02-resolve.t ├── 03-cache.t ├── 04-repopulate.t └── TestDNS.pm └── util └── lua-releng.pl /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/ 2 | t/error.log 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Hamish Forbes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | TEST_FILE ?= t 8 | 9 | .PHONY: all test leak 10 | 11 | all: ; 12 | 13 | 14 | install: all 15 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/dns 16 | 17 | leak: all 18 | TEST_NGINX_CHECK_LEAK=1 TEST_NGINX_NO_SHUFFLE=1 PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(TEST_FILE) 19 | 20 | test: all 21 | TEST_NGINX_NO_SHUFFLE=1 PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(TEST_FILE) 22 | util/lua-releng.pl 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-dns-cache 2 | 3 | A wrapper for [lua-resty-dns](https://github.com/openresty/lua-resty-dns) to cache responses based on record TTLs. 4 | 5 | Uses [lua-resty-lrucache](https://github.com/openresty/lua-resty-lrucache) and [ngx.shared.DICT](https://github.com/openresty/lua-nginx-module#ngxshareddict) to provide a 2 level cache. 6 | 7 | Can repopulate cache in the background while returning stale answers. 8 | 9 | # Overview 10 | 11 | ```lua 12 | lua_shared_dict dns_cache 1m; 13 | 14 | init_by_lua ' 15 | require("resty.dns.cache").init_cache(200) 16 | '; 17 | 18 | server { 19 | 20 | listen 80; 21 | server_name dns_cache; 22 | 23 | location / { 24 | content_by_lua ' 25 | local DNS_Cache = require("resty.dns.cache") 26 | local dns = DNS_Cache.new({ 27 | dict = "dns_cache", 28 | negative_ttl = 30, 29 | max_stale = 300, 30 | resolver = { 31 | nameservers = {"123.123.123.123"} 32 | } 33 | }) 34 | 35 | local host = ngx.req.get_uri_args()["host"] or "www.google.com" 36 | 37 | local answer, err, stale = dns:query(host) 38 | if err then 39 | if stale then 40 | ngx.header["Warning"] = "110: Response is stale" 41 | answer = stale 42 | ngx.log(ngx.ERR, err) 43 | else 44 | ngx.status = 500 45 | ngx.say(err) 46 | return ngx.exit(ngx.status) 47 | end 48 | end 49 | 50 | local cjson = require "cjson" 51 | ngx.say(cjson.encode(answer)) 52 | '; 53 | } 54 | } 55 | ``` 56 | 57 | # Methods 58 | ### init_cache 59 | `syntax: ok, err = dns_cache.init_cache(max_items?)` 60 | 61 | Creates a global lrucache object for caching responses. 62 | 63 | Accepts an optional `max_items` argument, defaults to 200 entries. 64 | 65 | Calling this repeatedly will reset the LRU cache 66 | 67 | ### initted 68 | `syntax: ok = dns_cache.initted()` 69 | 70 | Returns `true` if LRU Cache has been initialised 71 | 72 | ### new 73 | `syntax: ok, err = dns_cache.new(opts)` 74 | 75 | Returns a new DNS cache instance. Returns `nil` and a string on error 76 | 77 | Accepts a table of options, if no shared dictionary is provided only lrucache is used. 78 | 79 | * `dict` - Name of the [ngx.shared.DICT](https://github.com/openresty/lua-nginx-module#ngxshareddict) to use for cache. 80 | * `resolver` - Table of options passed to [lua-resty-dns](https://github.com/openresty/lua-resty-dns#new). Defaults to using Google DNS. 81 | * `normalise_ttl` - Boolean. Reduces TTL in cached answers to account for cached time. Defaults to `true`. 82 | * `negative_ttl` - Time in seconds to cache negative / error responses. `nil` or `false` disables caching negative responses. Defaults to `false` 83 | * `minimise_ttl` - Boolean. Set cache TTL based on the shortest DNS TTL in all responses rather than the first response. Defaults to `false` 84 | * `max_stale` - Number of seconds past expiry to return stale content rather than querying. Stale hits will trigger a non-blocking background query to repopulate cache. 85 | 86 | 87 | ### query 88 | `syntax: answers, err, stale = c:query(name, opts?)` 89 | 90 | Passes through to lua-resty-dns' [query](https://github.com/openresty/lua-resty-dns#query) method. 91 | 92 | Returns an extra `stale` variable containing stale data if a resolver cannot be contacted. 93 | 94 | ### tcp_query 95 | `syntax: answers, err, stale = c:tcp_query(name, opts?)` 96 | 97 | Passes through to lua-resty-dns' [tcp_query](https://github.com/openresty/lua-resty-dns#tcp_query) method. 98 | 99 | Returns an extra `stale` variable containing stale data if a resolver cannot be contacted. 100 | 101 | ### set_timeout 102 | `syntax: c:set_timeout(time)` 103 | 104 | Passes through to lua-resty-dns' [set_timeout](https://github.com/openresty/lua-resty-dns#set_timeout) method. 105 | 106 | ## Constants 107 | lua-resty-dns' [constants](https://github.com/openresty/lua-resty-dns#constants) are accessible on the `resty.dns.cache` object too. 108 | 109 | ## TODO 110 | * Cap'n'proto serialisation 111 | 112 | -------------------------------------------------------------------------------- /lib/resty/dns/cache.lua: -------------------------------------------------------------------------------- 1 | local ngx_log = ngx.log 2 | local ngx_DEBUG = ngx.DEBUG 3 | local ngx_ERR = ngx.ERR 4 | local ngx_shared = ngx.shared 5 | local ngx_time = ngx.time 6 | local resty_resolver = require "resty.dns.resolver" 7 | local resty_lrucache = require "resty.lrucache" 8 | local cjson = require "cjson" 9 | local json_encode = cjson.encode 10 | local json_decode = cjson.decode 11 | local tbl_concat = table.concat 12 | local tonumber = tonumber 13 | local _ngx_timer_at = ngx.timer.at 14 | local ngx_worker_pid = ngx.worker.pid 15 | 16 | local function ngx_timer_at(delay, func, ...) 17 | local ok, err = _ngx_timer_at(delay, func, ...) 18 | if not ok then 19 | ngx_log(ngx_ERR, "Timer Error: ", err) 20 | end 21 | return ok, err 22 | end 23 | 24 | 25 | local debug_log = function(msg, ...) 26 | if type(msg) == 'table' then 27 | local ok, json = pcall(json_encode, msg) 28 | if ok then 29 | msg = json 30 | else 31 | ngx_log(ngx_ERR, json) 32 | end 33 | end 34 | ngx_log(ngx_DEBUG, msg, ...) 35 | end 36 | 37 | local _M = { 38 | _VERSION = '0.01', 39 | TYPE_A = resty_resolver.TYPE_A, 40 | TYPE_NS = resty_resolver.TYPE_NS, 41 | TYPE_CNAME = resty_resolver.TYPE_CNAME, 42 | TYPE_PTR = resty_resolver.TYPE_PTR, 43 | TYPE_MX = resty_resolver.TYPE_MX, 44 | TYPE_TXT = resty_resolver.TYPE_TXT, 45 | TYPE_AAAA = resty_resolver.TYPE_AAAA, 46 | TYPE_SRV = resty_resolver.TYPE_SRV, 47 | TYPE_SPF = resty_resolver.TYPE_SPF, 48 | CLASS_IN = resty_resolver.CLASS_IN 49 | } 50 | 51 | local DEBUG = false 52 | 53 | local mt = { __index = _M } 54 | 55 | local lru_cache_defaults = {200} 56 | local resolver_defaults = { 57 | nameservers = {"8.8.8.8", "8.8.4.4"} 58 | } 59 | 60 | -- Global lrucache instance 61 | local lru_cache 62 | local max_items = 200 63 | 64 | function _M.init_cache(size) 65 | if size then max_items = size end 66 | local err 67 | if DEBUG then debug_log("Initialising lru cache with ", max_items, " max items") end 68 | lru_cache, err = resty_lrucache.new(max_items) 69 | if not lru_cache then 70 | return nil, err 71 | end 72 | return true 73 | end 74 | 75 | 76 | function _M.initted() 77 | if lru_cache then return true end 78 | return false 79 | end 80 | 81 | 82 | function _M.new(opts) 83 | local self, err = { opts = opts}, nil 84 | opts = opts or {} 85 | 86 | -- Set defaults 87 | if opts.normalise_ttl ~= nil then self.normalise_ttl = opts.normalise_ttl else self.normalise_ttl = true end 88 | if opts.minimise_ttl ~= nil then self.minimise_ttl = opts.minimise_ttl else self.minimise_ttl = false end 89 | if opts.negative_ttl ~= nil then 90 | self.negative_ttl = tonumber(opts.negative_ttl) 91 | else 92 | self.negative_ttl = false 93 | end 94 | if opts.max_stale ~= nil then 95 | self.max_stale = tonumber(opts.max_stale) 96 | else 97 | self.max_stale = 0 98 | end 99 | 100 | opts.resolver = opts.resolver or resolver_defaults 101 | self.resolver, err = resty_resolver:new(opts.resolver) 102 | if not self.resolver then 103 | return nil, err 104 | end 105 | 106 | if opts.dict then 107 | self.dict = ngx_shared[opts.dict] 108 | end 109 | return setmetatable(self, mt) 110 | end 111 | 112 | 113 | function _M.flush(self, hard) 114 | local ok, err = self.init_cache() 115 | if not ok then 116 | ngx_log(ngx_ERR, err) 117 | end 118 | if self.dict then 119 | if DEBUG then debug_log("Flushing dictionary") end 120 | self.dict:flush_all() 121 | if hard then 122 | local flushed = self.dict:flush_expired() 123 | if DEBUG then debug_log("Flushed ", flushed, " keys from memory") end 124 | end 125 | end 126 | end 127 | 128 | 129 | function _M._debug(flag) 130 | DEBUG = flag 131 | end 132 | 133 | 134 | function _M.set_timeout(self, ...) 135 | return self.resolver:set_timeout(...) 136 | end 137 | 138 | 139 | local function minimise_ttl(answer) 140 | if DEBUG then debug_log('Minimising TTL') end 141 | local ttl 142 | for _, ans in ipairs(answer) do 143 | if DEBUG then debug_log('TTL ', ans.name, ': ', ans.ttl) end 144 | if ttl == nil or ans.ttl < ttl then 145 | ttl = ans.ttl 146 | end 147 | end 148 | return ttl 149 | end 150 | 151 | 152 | local function normalise_ttl(self, data) 153 | -- Calculate time since query and subtract from answer's TTL 154 | if self.normalise_ttl then 155 | local now = ngx_time() 156 | local diff = now - data.now 157 | if DEBUG then debug_log("Normalising TTL, diff: ", diff) end 158 | for _, answer in ipairs(data.answer) do 159 | if DEBUG then debug_log("Old: ", answer.ttl, ", new: ", answer.ttl - diff) end 160 | answer.ttl = answer.ttl - diff 161 | end 162 | data.now = now 163 | end 164 | return data 165 | end 166 | 167 | 168 | local function cache_get(self, key) 169 | -- Try local LRU cache first 170 | local data, lru_stale 171 | if lru_cache then 172 | data, lru_stale = lru_cache:get(key) 173 | -- Set stale if should have expired 174 | if data and data.expires <= ngx_time() then 175 | lru_stale = data 176 | data = nil 177 | end 178 | if data then 179 | if DEBUG then 180 | debug_log('lru_cache HIT: ', key) 181 | debug_log(data) 182 | end 183 | return normalise_ttl(self, data) 184 | elseif DEBUG then 185 | debug_log('lru_cache MISS: ', key) 186 | end 187 | end 188 | 189 | -- lru_cache miss, try shared dict 190 | local dict = self.dict 191 | if dict then 192 | local data, flags, stale = dict:get_stale(key) 193 | -- Set stale if should have expired 194 | if data then 195 | data = json_decode(data) 196 | if data.expires <= ngx_time() then 197 | stale = true 198 | end 199 | end 200 | 201 | -- Dict data is stale, prefer stale LRU data 202 | if stale and lru_stale then 203 | if DEBUG then 204 | debug_log('lru_cache STALE: ', key) 205 | debug_log(lru_stale) 206 | end 207 | return nil, normalise_ttl(self, lru_stale) 208 | end 209 | 210 | -- Definitely no lru data, going to have to try shared dict 211 | if not data then 212 | -- Full MISS on dict, return nil 213 | if DEBUG then debug_log('shared_dict MISS: ', key) end 214 | return nil 215 | end 216 | 217 | -- Return nil and dict cache if its stale 218 | if stale then 219 | if DEBUG then debug_log('shared_dict STALE: ', key) end 220 | return nil, normalise_ttl(self, data) 221 | end 222 | 223 | -- Fresh HIT from dict, repopulate the lru_cache 224 | if DEBUG then debug_log('shared_dict HIT: ', key) end 225 | if lru_cache then 226 | local ttl = data.expires - ngx_time() 227 | if DEBUG then debug_log('lru_cache SET: ', key, ' ', ttl) end 228 | lru_cache:set(key, data, ttl) 229 | end 230 | return normalise_ttl(self, data) 231 | elseif lru_stale then 232 | -- Return lru stale if no dict configured 233 | if DEBUG then 234 | debug_log('lru_cache STALE: ', key) 235 | debug_log(lru_stale) 236 | end 237 | return nil, normalise_ttl(self, lru_stale) 238 | end 239 | 240 | if not lru_cache or dict then 241 | ngx_log(ngx_ERR, "No cache defined") 242 | end 243 | end 244 | 245 | 246 | local function cache_set(self, key, answer, ttl) 247 | -- Don't cache records with 0 TTL 248 | if ttl == 0 or ttl == nil then 249 | return 250 | end 251 | 252 | -- Calculate absolute expiry - used to populate lru_cache from shared_dict 253 | local now = ngx_time() 254 | local data = { 255 | answer = answer, 256 | now = now, 257 | queried = now, 258 | expires = now + ttl 259 | } 260 | 261 | -- Extend cache expiry if using stale 262 | local real_ttl = ttl 263 | if self.max_stale then 264 | real_ttl = real_ttl + self.max_stale 265 | end 266 | 267 | -- Set lru cache 268 | if lru_cache then 269 | if DEBUG then debug_log('lru_cache SET: ', key, ' ', real_ttl) end 270 | lru_cache:set(key, data, real_ttl) 271 | end 272 | 273 | -- Set dict cache 274 | local dict = self.dict 275 | if dict then 276 | if DEBUG then debug_log('shared_dict SET: ', key, ' ', real_ttl) end 277 | local ok, err, forcible = dict:set(key, json_encode(data), real_ttl) 278 | if not ok then 279 | ngx_log(ngx_ERR, 'shared_dict ERR: ', err) 280 | end 281 | if forcible then 282 | ngx_log(ngx_DEBUG, 'shared_dict full') 283 | end 284 | end 285 | end 286 | 287 | 288 | local function _resolve(resolver, query_func, host, opts) 289 | if DEBUG then debug_log('Querying: ', host) end 290 | local answers, err = query_func(resolver, host, opts) 291 | if not answers then 292 | return answers, err 293 | end 294 | if DEBUG then debug_log(answers) end 295 | 296 | return answers 297 | end 298 | 299 | 300 | local function cache_key(host, qtype) 301 | return tbl_concat({host,'|',qtype}) 302 | end 303 | 304 | 305 | local function get_repopulate_lock(dict, host, qtype) 306 | local key = cache_key(host, qtype or 1) .. '|lock' 307 | if DEBUG then debug_log("Locking '", key, "' for ", 30, "s: ", ngx_worker_pid()) end 308 | return dict:add(key, ngx_worker_pid(), 30) 309 | end 310 | 311 | 312 | local function release_repopulate_lock(dict, host, qtype) 313 | local key = cache_key(host, qtype or 1) .. '|lock' 314 | local pid, err = dict:get(key) 315 | if DEBUG then debug_log("Releasing '", key, "' for ", ngx_worker_pid(), " from ", pid) end 316 | if pid == ngx_worker_pid() then 317 | dict:delete(key) 318 | else 319 | ngx_log(ngx_DEBUG, "couldnt release lock") 320 | end 321 | end 322 | 323 | 324 | local _query 325 | 326 | local function _repopulate(premature, self, host, opts, tcp) 327 | if premature then return end 328 | 329 | if DEBUG then debug_log("Repopulating '", host, "'") end 330 | -- Create a new resolver instance, cannot share sockets 331 | local err 332 | self.resolver, err = resty_resolver:new(self.opts.resolver) 333 | if err then 334 | ngx_log(ngx_ERR, err) 335 | return nil 336 | end 337 | -- Do not use stale when repopulating 338 | _query(self, host, opts, tcp, true) 339 | end 340 | 341 | 342 | local function repopulate(self, host, opts, tcp) 343 | -- Lock, there's a window between the key expiring and the background query completing 344 | -- during which another query could trigger duplicate repopulate jobs 345 | local ok, err = get_repopulate_lock(self.dict, host, opts.qtype) 346 | if ok then 347 | if DEBUG then debug_log("Attempting to repopulate '", host, "'") end 348 | local ok, err = ngx_timer_at(0, _repopulate, self, host, opts, tcp) 349 | if not ok then 350 | -- Release lock if we couldn't start the timer 351 | release_repopulate_lock(self.dict, host, opts.qtype) 352 | end 353 | else 354 | if err == "exists" then 355 | if DEBUG then debug_log("Lock not acquired") end 356 | return 357 | else 358 | ngx.log(ngx.ERR, err) 359 | end 360 | end 361 | end 362 | 363 | 364 | _query = function(self, host, opts, tcp, repopulating) 365 | -- Build cache key 366 | opts = opts or {} 367 | local key = cache_key(host, opts.qtype or 1) 368 | 369 | -- Check caches 370 | local answer 371 | local data, stale = cache_get(self, key) 372 | if data then 373 | -- Shouldn't get a cache hit when repopulating but better safe than sorry 374 | if repopulating then release_repopulate_lock(self.dict, host, opts.qtype) end 375 | answer = data.answer 376 | -- Don't return negative cache hits if negative_ttl is off in this instance 377 | if not answer.errcode or self.negative_ttl then 378 | return answer 379 | end 380 | end 381 | 382 | -- No fresh cache entry, return stale if within max_stale and trigger background repopulate 383 | if stale and not repopulating and self.max_stale > 0 384 | and (ngx_time() - stale.expires) < self.max_stale then 385 | if DEBUG then debug_log('max_stale ', self.max_stale) end 386 | repopulate(self, host, opts, tcp) 387 | if DEBUG then debug_log('Returning STALE: ', key) end 388 | return nil, nil, stale.answer 389 | end 390 | 391 | -- Try to resolve 392 | local resolver = self.resolver 393 | local query_func = resolver.query 394 | if tcp then 395 | query_func = resolver.tcp_query 396 | end 397 | 398 | local answer, err = _resolve(resolver, query_func, host, opts) 399 | if not answer then 400 | -- Couldn't resolve, return potential stale response with error msg 401 | if DEBUG then 402 | debug_log('Resolver error ', key, ': ', err) 403 | if stale then debug_log('Returning STALE: ', key) end 404 | end 405 | if repopulating then release_repopulate_lock(self.dict, host, opts.qtype) end 406 | if stale then stale = stale.answer end 407 | return nil, err, stale 408 | end 409 | 410 | local ttl 411 | 412 | -- Cache server errors for negative_cache seconds 413 | if answer.errcode then 414 | if self.negative_ttl then 415 | ttl = self.negative_ttl 416 | else 417 | if repopulating then release_repopulate_lock(self.dict, host, opts.qtype) end 418 | return answer 419 | end 420 | else 421 | -- Cache for the lowest TTL in the chain of responses... 422 | if self.minimise_ttl then 423 | ttl = minimise_ttl(answer) 424 | elseif answer[1] then 425 | -- ... or just the first one 426 | ttl = answer[1].ttl or nil 427 | end 428 | end 429 | 430 | -- Set cache 431 | cache_set(self, key, answer, ttl) 432 | 433 | if repopulating then release_repopulate_lock(self.dict, host, opts.qtype) end 434 | 435 | return answer 436 | end 437 | 438 | 439 | function _M.query(self, host, opts) 440 | return _query(self, host, opts, false) 441 | end 442 | 443 | 444 | function _M.tcp_query(self, host, opts) 445 | return _query(self, host, opts, true) 446 | end 447 | 448 | 449 | return _M -------------------------------------------------------------------------------- /t/01-sanity.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | use Cwd qw(cwd); 3 | 4 | plan tests => repeat_each() * 24; 5 | 6 | my $pwd = cwd(); 7 | 8 | our $HttpConfig = qq{ 9 | lua_package_path "$pwd/lib/?.lua;;"; 10 | }; 11 | 12 | no_long_string(); 13 | run_tests(); 14 | 15 | __DATA__ 16 | === TEST 1: Load module without errors. 17 | --- http_config eval 18 | "$::HttpConfig" 19 | . q{ 20 | init_by_lua ' 21 | local DNS_Cache = require("resty.dns.cache") 22 | '; 23 | } 24 | --- config 25 | location /sanity { 26 | echo "OK"; 27 | } 28 | --- request 29 | GET /sanity 30 | --- no_error_log 31 | [error] 32 | --- response_body 33 | OK 34 | 35 | 36 | === TEST 2: Can init cache - defaults 37 | --- http_config eval 38 | "$::HttpConfig" 39 | . q{ 40 | init_by_lua ' 41 | local DNS_Cache = require("resty.dns.cache") 42 | DNS_Cache.init_cache() 43 | '; 44 | } 45 | --- config 46 | location /sanity { 47 | content_by_lua ' 48 | local DNS_Cache = require("resty.dns.cache") 49 | ngx.say(DNS_Cache.initted()) 50 | '; 51 | } 52 | --- request 53 | GET /sanity 54 | --- no_error_log 55 | [error] 56 | --- response_body 57 | true 58 | 59 | === TEST 3: Can init cache - user config 60 | --- http_config eval 61 | "$::HttpConfig" 62 | . q{ 63 | init_by_lua ' 64 | local DNS_Cache = require("resty.dns.cache") 65 | DNS_Cache.init_cache(300) 66 | '; 67 | } 68 | --- config 69 | location /sanity { 70 | content_by_lua ' 71 | local DNS_Cache = require("resty.dns.cache") 72 | ngx.say(DNS_Cache.initted()) 73 | '; 74 | } 75 | --- request 76 | GET /sanity 77 | --- no_error_log 78 | [error] 79 | --- response_body 80 | true 81 | 82 | === TEST 4: Can init new instance - defaults 83 | --- http_config eval 84 | "$::HttpConfig" 85 | . q{ 86 | init_by_lua ' 87 | local DNS_Cache = require("resty.dns.cache") 88 | DNS_Cache.init_cache(300) 89 | '; 90 | } 91 | --- config 92 | location /sanity { 93 | content_by_lua ' 94 | local DNS_Cache = require("resty.dns.cache") 95 | local dns, err = DNS_Cache.new() 96 | if dns then 97 | ngx.say("OK") 98 | else 99 | ngx.say(err) 100 | end 101 | '; 102 | } 103 | --- request 104 | GET /sanity 105 | --- no_error_log 106 | [error] 107 | --- response_body 108 | OK 109 | 110 | === TEST 5: Can init new instance - user config 111 | --- http_config eval 112 | "$::HttpConfig" 113 | . q{ 114 | init_by_lua ' 115 | local DNS_Cache = require("resty.dns.cache") 116 | DNS_Cache.init_cache(300) 117 | '; 118 | } 119 | --- config 120 | location /sanity { 121 | content_by_lua ' 122 | local DNS_Cache = require("resty.dns.cache") 123 | local dns, err = DNS_Cache.new({ 124 | negative_ttl = 10, 125 | resolver = { nameservers = {"10.10.10.10"} } 126 | }) 127 | if dns then 128 | ngx.say("OK") 129 | else 130 | ngx.say(err) 131 | end 132 | '; 133 | } 134 | --- request 135 | GET /sanity 136 | --- no_error_log 137 | [error] 138 | --- response_body 139 | OK 140 | 141 | === TEST 6: Resty DNS errors are passed through 142 | --- http_config eval 143 | "$::HttpConfig" 144 | . q{ 145 | init_by_lua ' 146 | local DNS_Cache = require("resty.dns.cache") 147 | DNS_Cache.init_cache(300) 148 | '; 149 | } 150 | --- config 151 | location /sanity { 152 | content_by_lua ' 153 | local DNS_Cache = require("resty.dns.cache") 154 | local dns, err = DNS_Cache.new({ 155 | resolver = { } 156 | }) 157 | if dns then 158 | ngx.say("OK") 159 | else 160 | ngx.say(err) 161 | end 162 | '; 163 | } 164 | --- request 165 | GET /sanity 166 | --- no_error_log 167 | [error] 168 | --- response_body 169 | no nameservers specified 170 | 171 | === TEST 7: Can create instance with shared dict 172 | --- http_config eval 173 | "$::HttpConfig" 174 | . q{ 175 | lua_shared_dict dns_cache 1m; 176 | init_by_lua ' 177 | local DNS_Cache = require("resty.dns.cache") 178 | DNS_Cache.init_cache() 179 | '; 180 | } 181 | --- config 182 | location /sanity { 183 | content_by_lua ' 184 | local DNS_Cache = require("resty.dns.cache") 185 | ngx.say(DNS_Cache.initted()) 186 | 187 | local dns, err = DNS_Cache.new({ 188 | dict = "dns_cache" 189 | }) 190 | if dns then 191 | ngx.say("OK") 192 | else 193 | ngx.say(err) 194 | end 195 | '; 196 | } 197 | --- request 198 | GET /sanity 199 | --- no_error_log 200 | [error] 201 | --- response_body 202 | true 203 | OK 204 | 205 | === TEST 8: Can create instance with shared dict and no lru_cache 206 | --- http_config eval 207 | "$::HttpConfig" 208 | . q{ 209 | lua_shared_dict dns_cache 1m; 210 | } 211 | --- config 212 | location /sanity { 213 | content_by_lua ' 214 | local DNS_Cache = require("resty.dns.cache") 215 | ngx.say(DNS_Cache.initted()) 216 | 217 | local dns, err = DNS_Cache.new({ 218 | dict = "dns_cache" 219 | }) 220 | if dns then 221 | ngx.say("OK") 222 | else 223 | ngx.say(err) 224 | end 225 | '; 226 | } 227 | --- request 228 | GET /sanity 229 | --- no_error_log 230 | [error] 231 | --- response_body 232 | false 233 | OK 234 | -------------------------------------------------------------------------------- /t/02-resolve.t: -------------------------------------------------------------------------------- 1 | 2 | use lib 't'; 3 | use TestDNS; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 12; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | }; 13 | 14 | no_long_string(); 15 | run_tests(); 16 | 17 | __DATA__ 18 | === TEST 1: Can resolve with lru + dict 19 | --- http_config eval 20 | "$::HttpConfig" 21 | . q{ 22 | lua_shared_dict dns_cache 1m; 23 | init_by_lua ' 24 | local DNS_Cache = require("resty.dns.cache") 25 | DNS_Cache.init_cache() 26 | '; 27 | } 28 | --- config 29 | location /t { 30 | content_by_lua ' 31 | local DNS_Cache = require("resty.dns.cache") 32 | local dns, err = DNS_Cache.new({ 33 | dict = "dns_cache", 34 | resolver = { 35 | nameservers = { {"127.0.0.1", "1953"} } 36 | } 37 | }) 38 | if not dns then 39 | ngx.say(err) 40 | end 41 | dns.resolver._id = 125 42 | 43 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 44 | if not answer then 45 | ngx.say(err) 46 | end 47 | local cjson = require"cjson" 48 | ngx.say(cjson.encode(answer)) 49 | '; 50 | } 51 | --- udp_listen: 1953 52 | --- udp_reply dns 53 | { 54 | id => 125, 55 | opcode => 0, 56 | qname => 'www.google.com', 57 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 58 | } 59 | --- request 60 | GET /t 61 | --- no_error_log 62 | [error] 63 | --- response_body 64 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 65 | 66 | === TEST 2: Can resolve with lru only 67 | --- http_config eval 68 | "$::HttpConfig" 69 | . q{ 70 | init_by_lua ' 71 | local DNS_Cache = require("resty.dns.cache") 72 | DNS_Cache.init_cache() 73 | '; 74 | } 75 | --- config 76 | location /t { 77 | content_by_lua ' 78 | local DNS_Cache = require("resty.dns.cache") 79 | local dns, err = DNS_Cache.new({ 80 | resolver = { 81 | nameservers = { {"127.0.0.1", "1953"} } 82 | } 83 | }) 84 | if not dns then 85 | ngx.say(err) 86 | end 87 | dns.resolver._id = 125 88 | 89 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 90 | if not answer then 91 | ngx.say(err) 92 | end 93 | local cjson = require"cjson" 94 | ngx.say(cjson.encode(answer)) 95 | '; 96 | } 97 | --- udp_listen: 1953 98 | --- udp_reply dns 99 | { 100 | id => 125, 101 | opcode => 0, 102 | qname => 'www.google.com', 103 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 104 | } 105 | --- request 106 | GET /t 107 | --- no_error_log 108 | [error] 109 | --- response_body 110 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 111 | 112 | === TEST 3: Can resolve with dict only 113 | --- http_config eval 114 | "$::HttpConfig" 115 | . q{ 116 | lua_shared_dict dns_cache 1m; 117 | } 118 | --- config 119 | location /t { 120 | content_by_lua ' 121 | local DNS_Cache = require("resty.dns.cache") 122 | local dns, err = DNS_Cache.new({ 123 | dict = "dns_cache", 124 | resolver = { 125 | nameservers = { {"127.0.0.1", "1953"} } 126 | } 127 | }) 128 | if not dns then 129 | ngx.say(err) 130 | end 131 | dns.resolver._id = 125 132 | 133 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 134 | if not answer then 135 | ngx.say(err) 136 | end 137 | local cjson = require"cjson" 138 | ngx.say(cjson.encode(answer)) 139 | '; 140 | } 141 | --- udp_listen: 1953 142 | --- udp_reply dns 143 | { 144 | id => 125, 145 | opcode => 0, 146 | qname => 'www.google.com', 147 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 148 | } 149 | --- request 150 | GET /t 151 | --- no_error_log 152 | [error] 153 | --- response_body 154 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 155 | 156 | === TEST 4: Can resolve with no cache, error thrown 157 | --- http_config eval 158 | "$::HttpConfig" 159 | --- config 160 | location /t { 161 | content_by_lua ' 162 | local DNS_Cache = require("resty.dns.cache") 163 | local dns, err = DNS_Cache.new({ 164 | resolver = { 165 | nameservers = { {"127.0.0.1", "1953"} } 166 | } 167 | }) 168 | if not dns then 169 | ngx.say(err) 170 | end 171 | dns.resolver._id = 125 172 | 173 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 174 | if not answer then 175 | ngx.say(err) 176 | end 177 | local cjson = require"cjson" 178 | ngx.say(cjson.encode(answer)) 179 | '; 180 | } 181 | --- udp_listen: 1953 182 | --- udp_reply dns 183 | { 184 | id => 125, 185 | opcode => 0, 186 | qname => 'www.google.com', 187 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 188 | } 189 | --- request 190 | GET /t 191 | --- error_log 192 | No cache defined 193 | --- response_body 194 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 195 | 196 | -------------------------------------------------------------------------------- /t/03-cache.t: -------------------------------------------------------------------------------- 1 | use lib 't'; 2 | use TestDNS; 3 | use Cwd qw(cwd); 4 | 5 | plan tests => repeat_each() * 47; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | lua_socket_log_errors off; 12 | }; 13 | 14 | no_long_string(); 15 | run_tests(); 16 | 17 | __DATA__ 18 | === TEST 1: Response comes from cache on second hit 19 | --- http_config eval 20 | "$::HttpConfig" 21 | . q{ 22 | lua_shared_dict dns_cache 1m; 23 | init_by_lua ' 24 | local DNS_Cache = require("resty.dns.cache") 25 | DNS_Cache.init_cache() 26 | '; 27 | } 28 | --- config 29 | location /t { 30 | echo_location /_t; 31 | echo_location /_t2; 32 | } 33 | location /_t { 34 | content_by_lua ' 35 | local DNS_Cache = require("resty.dns.cache") 36 | local dns, err = DNS_Cache.new({ 37 | dict = "dns_cache", 38 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 39 | }) 40 | if not dns then 41 | ngx.say(err) 42 | end 43 | dns.resolver._id = 125 44 | 45 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 46 | if not answer then 47 | ngx.say(err) 48 | end 49 | '; 50 | } 51 | location /_t2 { 52 | content_by_lua ' 53 | local DNS_Cache = require("resty.dns.cache") 54 | local dns, err = DNS_Cache.new({ 55 | dict = "dns_cache", 56 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 57 | }) 58 | if not dns then 59 | ngx.say(err) 60 | end 61 | dns.resolver._id = 125 62 | dns._debug(true) 63 | 64 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 65 | if not answer then 66 | ngx.say(err) 67 | end 68 | local cjson = require"cjson" 69 | ngx.say(cjson.encode(answer)) 70 | 71 | '; 72 | } 73 | --- udp_listen: 1953 74 | --- udp_reply dns 75 | { 76 | id => 125, 77 | opcode => 0, 78 | qname => 'www.google.com', 79 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 80 | } 81 | --- request 82 | GET /t 83 | --- error_log 84 | lru_cache HIT 85 | --- response_body 86 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 87 | 88 | === TEST 2: Response comes from dict on miss 89 | --- http_config eval 90 | "$::HttpConfig" 91 | . q{ 92 | lua_shared_dict dns_cache 1m; 93 | init_by_lua ' 94 | local DNS_Cache = require("resty.dns.cache") 95 | DNS_Cache.init_cache() 96 | '; 97 | } 98 | --- config 99 | location /t { 100 | echo_location /_t; 101 | echo_location /_t2; 102 | } 103 | location /_t { 104 | content_by_lua ' 105 | local DNS_Cache = require("resty.dns.cache") 106 | local dns, err = DNS_Cache.new({ 107 | dict = "dns_cache", 108 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 109 | }) 110 | if not dns then 111 | ngx.say(err) 112 | end 113 | dns.resolver._id = 125 114 | 115 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 116 | if not answer then 117 | ngx.say(err) 118 | end 119 | '; 120 | } 121 | location /_t2 { 122 | content_by_lua ' 123 | local DNS_Cache = require("resty.dns.cache") 124 | DNS_Cache.init_cache() -- reset cache 125 | local dns, err = DNS_Cache.new({ 126 | dict = "dns_cache", 127 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 128 | }) 129 | if not dns then 130 | ngx.say(err) 131 | end 132 | dns.resolver._id = 125 133 | dns._debug(true) 134 | 135 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 136 | if not answer then 137 | ngx.say(err) 138 | end 139 | local cjson = require"cjson" 140 | ngx.say(cjson.encode(answer)) 141 | 142 | '; 143 | } 144 | --- udp_listen: 1953 145 | --- udp_reply dns 146 | { 147 | id => 125, 148 | opcode => 0, 149 | qname => 'www.google.com', 150 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }], 151 | } 152 | --- request 153 | GET /t 154 | --- error_log 155 | lru_cache MISS 156 | shared_dict HIT 157 | lru_cache SET 158 | --- response_body 159 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456}] 160 | 161 | 162 | === TEST 3: Stale response from lru served if resolver down 163 | --- http_config eval 164 | "$::HttpConfig" 165 | . q{ 166 | lua_shared_dict dns_cache 1m; 167 | init_by_lua ' 168 | local DNS_Cache = require("resty.dns.cache") 169 | DNS_Cache.init_cache() 170 | '; 171 | } 172 | --- config 173 | location /t { 174 | echo_location /_t; 175 | echo_sleep 2; 176 | echo_location /_t2; 177 | } 178 | location /_t { 179 | content_by_lua ' 180 | local DNS_Cache = require("resty.dns.cache") 181 | local dns, err = DNS_Cache.new({ 182 | dict = "dns_cache", 183 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 184 | }) 185 | if not dns then 186 | ngx.say(err) 187 | end 188 | dns.resolver._id = 125 189 | 190 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 191 | if not answer then 192 | ngx.say(err) 193 | end 194 | '; 195 | } 196 | location /_t2 { 197 | content_by_lua ' 198 | local DNS_Cache = require("resty.dns.cache") 199 | local dns, err = DNS_Cache.new({ 200 | dict = "dns_cache", 201 | resolver = {nameservers = {{"127.0.0.1", "1954"}}, retrans = 1, timeout = 100} 202 | }) 203 | if not dns then 204 | ngx.say(err) 205 | end 206 | dns.resolver._id = 125 207 | dns._debug(true) 208 | 209 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 210 | if stale then 211 | answer = stale 212 | end 213 | local cjson = require"cjson" 214 | ngx.say(cjson.encode(answer)) 215 | 216 | '; 217 | } 218 | --- udp_listen: 1953 219 | --- udp_reply dns 220 | { 221 | id => 125, 222 | opcode => 0, 223 | qname => 'www.google.com', 224 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 225 | } 226 | --- request 227 | GET /t 228 | --- error_log 229 | lru_cache MISS 230 | lru_cache STALE 231 | --- response_body 232 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":-1}] 233 | 234 | 235 | === TEST 4: Stale response from dict served if resolver down 236 | --- http_config eval 237 | "$::HttpConfig" 238 | . q{ 239 | lua_shared_dict dns_cache 1m; 240 | init_by_lua ' 241 | local DNS_Cache = require("resty.dns.cache") 242 | DNS_Cache.init_cache() 243 | '; 244 | } 245 | --- config 246 | location /t { 247 | echo_location /_t; 248 | echo_sleep 2; 249 | echo_location /_t2; 250 | } 251 | location /_t { 252 | content_by_lua ' 253 | local DNS_Cache = require("resty.dns.cache") 254 | local dns, err = DNS_Cache.new({ 255 | dict = "dns_cache", 256 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 257 | }) 258 | if not dns then 259 | ngx.say(err) 260 | end 261 | dns.resolver._id = 125 262 | 263 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 264 | if not answer then 265 | ngx.say(err) 266 | end 267 | '; 268 | } 269 | location /_t2 { 270 | content_by_lua ' 271 | local DNS_Cache = require("resty.dns.cache") 272 | local dns, err = DNS_Cache.new({ 273 | dict = "dns_cache", 274 | resolver = {nameservers = {{"127.0.0.1", "1954"}}, retrans = 1, timeout = 100} 275 | }) 276 | DNS_Cache.init_cache() -- reset cache 277 | if not dns then 278 | ngx.say(err) 279 | end 280 | dns.resolver._id = 125 281 | dns._debug(true) 282 | 283 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 284 | if stale then 285 | answer = stale 286 | end 287 | local cjson = require"cjson" 288 | ngx.say(cjson.encode(answer)) 289 | '; 290 | } 291 | --- udp_listen: 1953 292 | --- udp_reply dns 293 | { 294 | id => 125, 295 | opcode => 0, 296 | qname => 'www.google.com', 297 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 298 | } 299 | --- request 300 | GET /t 301 | --- error_log 302 | lru_cache MISS 303 | shared_dict STALE 304 | --- response_body 305 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":-1}] 306 | 307 | === TEST 5: Stale response from lru served if resolver down, no dict 308 | --- http_config eval 309 | "$::HttpConfig" 310 | . q{ 311 | init_by_lua ' 312 | local DNS_Cache = require("resty.dns.cache") 313 | DNS_Cache.init_cache() 314 | '; 315 | } 316 | --- config 317 | location /t { 318 | echo_location /_t; 319 | echo_sleep 2; 320 | echo_location /_t2; 321 | } 322 | location /_t { 323 | content_by_lua ' 324 | local DNS_Cache = require("resty.dns.cache") 325 | local dns, err = DNS_Cache.new({ 326 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 327 | }) 328 | if not dns then 329 | ngx.say(err) 330 | end 331 | dns.resolver._id = 125 332 | 333 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 334 | if not answer then 335 | ngx.say(err) 336 | end 337 | '; 338 | } 339 | location /_t2 { 340 | content_by_lua ' 341 | local DNS_Cache = require("resty.dns.cache") 342 | local dns, err = DNS_Cache.new({ 343 | dict = "dns_cache", 344 | resolver = {nameservers = {{"127.0.0.1", "1954"}}, retrans = 1, timeout = 100} 345 | }) 346 | if not dns then 347 | ngx.say(err) 348 | end 349 | dns.resolver._id = 125 350 | dns._debug(true) 351 | 352 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 353 | if stale then 354 | answer = stale 355 | end 356 | local cjson = require"cjson" 357 | ngx.say(cjson.encode(answer)) 358 | 359 | '; 360 | } 361 | --- udp_listen: 1953 362 | --- udp_reply dns 363 | { 364 | id => 125, 365 | opcode => 0, 366 | qname => 'www.google.com', 367 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 368 | } 369 | --- request 370 | GET /t 371 | --- error_log 372 | lru_cache MISS 373 | lru_cache STALE 374 | --- response_body 375 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":-1}] 376 | 377 | === TEST 6: Stale response from dict served if resolver down, no lru 378 | --- http_config eval 379 | "$::HttpConfig" 380 | . q{ 381 | lua_shared_dict dns_cache 1m; 382 | } 383 | --- config 384 | location /t { 385 | echo_location /_t; 386 | echo_sleep 2; 387 | echo_location /_t2; 388 | } 389 | location /_t { 390 | content_by_lua ' 391 | local DNS_Cache = require("resty.dns.cache") 392 | local dns, err = DNS_Cache.new({ 393 | dict = "dns_cache", 394 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 395 | }) 396 | if not dns then 397 | ngx.say(err) 398 | end 399 | dns.resolver._id = 125 400 | 401 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 402 | if not answer then 403 | ngx.say(err) 404 | end 405 | '; 406 | } 407 | location /_t2 { 408 | content_by_lua ' 409 | local DNS_Cache = require("resty.dns.cache") 410 | local dns, err = DNS_Cache.new({ 411 | dict = "dns_cache", 412 | resolver = {nameservers = {{"127.0.0.1", "1954"}}, retrans = 1, timeout = 100} 413 | }) 414 | if not dns then 415 | ngx.say(err) 416 | end 417 | dns.resolver._id = 125 418 | dns._debug(true) 419 | 420 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 421 | if stale then 422 | answer = stale 423 | end 424 | local cjson = require"cjson" 425 | ngx.say(cjson.encode(answer)) 426 | '; 427 | } 428 | --- udp_listen: 1953 429 | --- udp_reply dns 430 | { 431 | id => 125, 432 | opcode => 0, 433 | qname => 'www.google.com', 434 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 435 | } 436 | --- request 437 | GET /t 438 | --- error_log 439 | shared_dict STALE 440 | --- response_body 441 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":-1}] 442 | 443 | === TEST 7: TTLs are reduced 444 | --- http_config eval 445 | "$::HttpConfig" 446 | . q{ 447 | lua_shared_dict dns_cache 1m; 448 | init_by_lua ' 449 | local DNS_Cache = require("resty.dns.cache") 450 | DNS_Cache.init_cache() 451 | '; 452 | } 453 | --- config 454 | location /t { 455 | echo_location /_t; 456 | echo_sleep 2; 457 | echo_location /_t2; 458 | } 459 | location /_t { 460 | content_by_lua ' 461 | local DNS_Cache = require("resty.dns.cache") 462 | local dns, err = DNS_Cache.new({ 463 | dict = "dns_cache", 464 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 465 | }) 466 | if not dns then 467 | ngx.say(err) 468 | end 469 | dns.resolver._id = 125 470 | 471 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 472 | if not answer then 473 | ngx.say(err) 474 | end 475 | '; 476 | } 477 | location /_t2 { 478 | content_by_lua ' 479 | local DNS_Cache = require("resty.dns.cache") 480 | local dns, err = DNS_Cache.new({ 481 | dict = "dns_cache", 482 | resolver = {nameservers = {{"127.0.0.1", "1953"}}, retrans = 1, timeout = 100} 483 | }) 484 | if not dns then 485 | ngx.say(err) 486 | end 487 | dns.resolver._id = 125 488 | 489 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 490 | if not answer then 491 | ngx.say(answer) 492 | end 493 | local cjson = require"cjson" 494 | ngx.say(cjson.encode(answer)) 495 | 496 | '; 497 | } 498 | --- udp_listen: 1953 499 | --- udp_reply dns 500 | { 501 | id => 125, 502 | opcode => 0, 503 | qname => 'www.google.com', 504 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 10 }], 505 | } 506 | --- request 507 | GET /t 508 | --- no_error_log 509 | [error] 510 | --- response_body 511 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":8}] 512 | 513 | === TEST 8: TTL reduction can be disabled 514 | --- http_config eval 515 | "$::HttpConfig" 516 | . q{ 517 | lua_shared_dict dns_cache 1m; 518 | init_by_lua ' 519 | local DNS_Cache = require("resty.dns.cache") 520 | DNS_Cache.init_cache() 521 | '; 522 | } 523 | --- config 524 | location /t { 525 | echo_location /_t; 526 | echo_sleep 2; 527 | echo_location /_t2; 528 | } 529 | location /_t { 530 | content_by_lua ' 531 | local DNS_Cache = require("resty.dns.cache") 532 | local dns, err = DNS_Cache.new({ 533 | dict = "dns_cache", 534 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 535 | }) 536 | if not dns then 537 | ngx.say(err) 538 | end 539 | dns.resolver._id = 125 540 | 541 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 542 | if not answer then 543 | ngx.say(err) 544 | end 545 | '; 546 | } 547 | location /_t2 { 548 | content_by_lua ' 549 | local DNS_Cache = require("resty.dns.cache") 550 | local dns, err = DNS_Cache.new({ 551 | dict = "dns_cache", 552 | normalise_ttl = false, 553 | resolver = {nameservers = {{"127.0.0.1", "1953"}}, retrans = 1, timeout = 100} 554 | }) 555 | if not dns then 556 | ngx.say(err) 557 | end 558 | dns.resolver._id = 125 559 | 560 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 561 | if not answer then 562 | ngx.say(answer) 563 | end 564 | local cjson = require"cjson" 565 | ngx.say(cjson.encode(answer)) 566 | 567 | '; 568 | } 569 | --- udp_listen: 1953 570 | --- udp_reply dns 571 | { 572 | id => 125, 573 | opcode => 0, 574 | qname => 'www.google.com', 575 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 10 }], 576 | } 577 | --- request 578 | GET /t 579 | --- no_error_log 580 | [error] 581 | --- response_body 582 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":10}] 583 | 584 | === TEST 9: Negative responses are not cached by default 585 | --- http_config eval 586 | "$::HttpConfig" 587 | . q{ 588 | lua_shared_dict dns_cache 1m; 589 | init_by_lua ' 590 | local DNS_Cache = require("resty.dns.cache") 591 | DNS_Cache.init_cache() 592 | '; 593 | } 594 | --- config 595 | location /t { 596 | content_by_lua ' 597 | local DNS_Cache = require("resty.dns.cache") 598 | local dns, err = DNS_Cache.new({ 599 | dict = "dns_cache", 600 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 601 | }) 602 | if not dns then 603 | ngx.say(err) 604 | end 605 | dns._debug(true) 606 | dns.resolver._id = 125 607 | 608 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 609 | if not answer then 610 | ngx.say(err) 611 | end 612 | local cjson = require"cjson" 613 | ngx.say(cjson.encode(answer)) 614 | '; 615 | } 616 | --- udp_listen: 1953 617 | --- udp_reply dns 618 | { 619 | id => 125, 620 | rcode => 3, 621 | opcode => 0, 622 | qname => 'www.google.com', 623 | } 624 | --- request 625 | GET /t 626 | --- no_error_log 627 | SET 628 | --- response_body 629 | {"errcode":3,"errstr":"name error"} 630 | 631 | 632 | === TEST 10: Negative responses can be cached 633 | --- http_config eval 634 | "$::HttpConfig" 635 | . q{ 636 | lua_shared_dict dns_cache 1m; 637 | init_by_lua ' 638 | local DNS_Cache = require("resty.dns.cache") 639 | DNS_Cache.init_cache() 640 | '; 641 | } 642 | --- config 643 | location /t { 644 | echo_location /_t; 645 | echo_location /_t2; 646 | } 647 | location /_t { 648 | content_by_lua ' 649 | local DNS_Cache = require("resty.dns.cache") 650 | local dns, err = DNS_Cache.new({ 651 | dict = "dns_cache", 652 | negative_ttl = 10, 653 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 654 | }) 655 | if not dns then 656 | ngx.say(err) 657 | end 658 | dns.resolver._id = 125 659 | 660 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 661 | if not answer then 662 | ngx.say(err) 663 | end 664 | '; 665 | } 666 | location /_t2 { 667 | content_by_lua ' 668 | local DNS_Cache = require("resty.dns.cache") 669 | local dns, err = DNS_Cache.new({ 670 | dict = "dns_cache", 671 | negative_ttl = 10, 672 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 673 | }) 674 | if not dns then 675 | ngx.say(err) 676 | end 677 | dns.resolver._id = 125 678 | dns._debug(true) 679 | 680 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 681 | if not answer then 682 | ngx.say(err) 683 | end 684 | local cjson = require"cjson" 685 | ngx.say(cjson.encode(answer)) 686 | '; 687 | } 688 | --- udp_listen: 1953 689 | --- udp_reply dns 690 | { 691 | id => 125, 692 | rcode => 3, 693 | opcode => 0, 694 | qname => 'www.google.com', 695 | } 696 | --- request 697 | GET /t 698 | --- error_log 699 | lru_cache HIT 700 | --- response_body 701 | {"errcode":3,"errstr":"name error"} 702 | 703 | === TEST 11: Cached negative responses are not returned by default 704 | --- http_config eval 705 | "$::HttpConfig" 706 | . q{ 707 | lua_shared_dict dns_cache 1m; 708 | init_by_lua ' 709 | local DNS_Cache = require("resty.dns.cache") 710 | DNS_Cache.init_cache() 711 | '; 712 | } 713 | --- config 714 | location /t { 715 | echo_location /_t; 716 | echo_location /_t2; 717 | } 718 | location /_t { 719 | content_by_lua ' 720 | local DNS_Cache = require("resty.dns.cache") 721 | local dns, err = DNS_Cache.new({ 722 | dict = "dns_cache", 723 | negative_ttl = 10, 724 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 725 | }) 726 | if not dns then 727 | ngx.say(err) 728 | end 729 | dns._debug(true) 730 | dns.resolver._id = 125 731 | 732 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 733 | if not answer then 734 | ngx.say(err) 735 | end 736 | '; 737 | } 738 | location /_t2 { 739 | content_by_lua ' 740 | local DNS_Cache = require("resty.dns.cache") 741 | local dns, err = DNS_Cache.new({ 742 | dict = "dns_cache", 743 | resolver = {nameservers = {{"127.0.0.1", "1954"}, retrans = 1, timeout = 100}} 744 | }) 745 | if not dns then 746 | ngx.say(err) 747 | end 748 | dns.resolver._id = 125 749 | dns._debug(true) 750 | 751 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 752 | local cjson = require"cjson" 753 | ngx.say(cjson.encode(answer)) 754 | '; 755 | } 756 | --- udp_listen: 1953 757 | --- udp_reply dns 758 | { 759 | id => 125, 760 | rcode => 3, 761 | opcode => 0, 762 | qname => 'www.google.com', 763 | } 764 | --- request 765 | GET /t 766 | --- error_log 767 | lru_cache SET 768 | lru_cache HIT 769 | --- response_body 770 | null 771 | 772 | === TEST 12: Cache TTL can be minimised 773 | --- http_config eval 774 | "$::HttpConfig" 775 | . q{ 776 | lua_shared_dict dns_cache 1m; 777 | init_by_lua ' 778 | local DNS_Cache = require("resty.dns.cache") 779 | DNS_Cache.init_cache() 780 | '; 781 | } 782 | --- config 783 | location /t { 784 | content_by_lua ' 785 | local DNS_Cache = require("resty.dns.cache") 786 | local dns, err = DNS_Cache.new({ 787 | dict = "dns_cache", 788 | minimise_ttl = true, 789 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 790 | }) 791 | if not dns then 792 | ngx.say(err) 793 | end 794 | dns.resolver._id = 125 795 | dns._debug(true) 796 | 797 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 798 | if not answer then 799 | ngx.say(err) 800 | end 801 | local cjson = require"cjson" 802 | ngx.say(cjson.encode(answer)) 803 | '; 804 | } 805 | --- udp_listen: 1953 806 | --- udp_reply dns 807 | { 808 | id => 125, 809 | opcode => 0, 810 | qname => 'www.google.com', 811 | answer => [ 812 | { name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }, 813 | { name => "l.www.google.com", ipv6 => "::1", ttl => 10 }, 814 | ], 815 | } 816 | --- request 817 | GET /t 818 | --- error_log 819 | lru_cache SET: www.google.com|1 10 820 | shared_dict SET: www.google.com|1 10 821 | --- response_body 822 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456},{"address":"0:0:0:0:0:0:0:1","type":28,"class":1,"name":"l.www.google.com","ttl":10}] 823 | 824 | === TEST 13: Cache TTLs not minimised by default 825 | --- http_config eval 826 | "$::HttpConfig" 827 | . q{ 828 | lua_shared_dict dns_cache 1m; 829 | init_by_lua ' 830 | local DNS_Cache = require("resty.dns.cache") 831 | DNS_Cache.init_cache() 832 | '; 833 | } 834 | --- config 835 | location /t { 836 | content_by_lua ' 837 | local DNS_Cache = require("resty.dns.cache") 838 | local dns, err = DNS_Cache.new({ 839 | dict = "dns_cache", 840 | resolver = {nameservers = {{"127.0.0.1", "1953"}}} 841 | }) 842 | if not dns then 843 | ngx.say(err) 844 | end 845 | dns.resolver._id = 125 846 | dns._debug(true) 847 | 848 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 849 | if not answer then 850 | ngx.say(err) 851 | end 852 | local cjson = require"cjson" 853 | ngx.say(cjson.encode(answer)) 854 | '; 855 | } 856 | --- udp_listen: 1953 857 | --- udp_reply dns 858 | { 859 | id => 125, 860 | opcode => 0, 861 | qname => 'www.google.com', 862 | answer => [ 863 | { name => "www.google.com", ipv4 => "127.0.0.1", ttl => 123456 }, 864 | { name => "l.www.google.com", ipv6 => "::1", ttl => 10 }, 865 | ], 866 | } 867 | --- request 868 | GET /t 869 | --- error_log 870 | lru_cache SET: www.google.com|1 123456 871 | shared_dict SET: www.google.com|1 123456 872 | --- response_body 873 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":123456},{"address":"0:0:0:0:0:0:0:1","type":28,"class":1,"name":"l.www.google.com","ttl":10}] 874 | -------------------------------------------------------------------------------- /t/04-repopulate.t: -------------------------------------------------------------------------------- 1 | use lib 't'; 2 | use TestDNS; 3 | use Cwd qw(cwd); 4 | 5 | plan tests => repeat_each() * 17; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | lua_socket_log_errors off; 12 | }; 13 | 14 | no_long_string(); 15 | run_tests(); 16 | 17 | __DATA__ 18 | === TEST 1: Query is triggered when cache is expired 19 | --- http_config eval 20 | "$::HttpConfig" 21 | . q{ 22 | lua_shared_dict dns_cache 1m; 23 | init_by_lua ' 24 | local DNS_Cache = require("resty.dns.cache") 25 | DNS_Cache.init_cache() 26 | '; 27 | } 28 | --- config 29 | location /t { 30 | content_by_lua ' 31 | local DNS_Cache = require("resty.dns.cache") 32 | local dns, err = DNS_Cache.new({ 33 | dict = "dns_cache", 34 | resolver = {nameservers = {{"127.0.0.1", "1953"}}}, 35 | max_stale = 10 36 | }) 37 | if not dns then 38 | ngx.say(err) 39 | end 40 | dns.resolver._id = 125 41 | 42 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 43 | if not answer then 44 | ngx.say(err) 45 | end 46 | 47 | local cjson = require"cjson" 48 | ngx.say(cjson.encode(answer)) 49 | 50 | dns._debug(true) 51 | 52 | -- Sleep beyond response TTL 53 | ngx.sleep(1.1) 54 | 55 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 56 | if not answer then 57 | if stale then 58 | answer = stale 59 | else 60 | ngx.say(err) 61 | end 62 | end 63 | 64 | local cjson = require"cjson" 65 | ngx.say(cjson.encode(answer)) 66 | 67 | ngx.sleep(0.1) 68 | 69 | '; 70 | } 71 | --- udp_listen: 1953 72 | --- udp_reply dns 73 | { 74 | id => 125, 75 | opcode => 0, 76 | qname => 'www.google.com', 77 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 78 | } 79 | --- request 80 | GET /t 81 | --- error_log 82 | Returning STALE 83 | Attempting to repopulate 'www.google.com' 84 | Repopulating 'www.google.com' 85 | --- response_body 86 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":1}] 87 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":0}] 88 | 89 | === TEST 2: Query is not triggered when cache expires and max_stale is disabled 90 | --- http_config eval 91 | "$::HttpConfig" 92 | . q{ 93 | lua_shared_dict dns_cache 1m; 94 | init_by_lua ' 95 | local DNS_Cache = require("resty.dns.cache") 96 | DNS_Cache.init_cache() 97 | '; 98 | } 99 | --- config 100 | location /t { 101 | content_by_lua ' 102 | local DNS_Cache = require("resty.dns.cache") 103 | local dns, err = DNS_Cache.new({ 104 | dict = "dns_cache", 105 | resolver = {nameservers = {{"127.0.0.1", "1953"}}, retrans = 1, timeout = 50 }, 106 | max_stale = 0 107 | }) 108 | if not dns then 109 | ngx.say(err) 110 | end 111 | dns.resolver._id = 125 112 | 113 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 114 | if not answer then 115 | ngx.say(err) 116 | end 117 | 118 | dns._debug(true) 119 | 120 | -- Sleep beyond response TTL 121 | ngx.sleep(1.1) 122 | 123 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 124 | if not answer then 125 | if stale then 126 | answer = stale 127 | else 128 | ngx.say(err) 129 | end 130 | end 131 | 132 | local cjson = require"cjson" 133 | ngx.say(cjson.encode(answer)) 134 | 135 | ngx.sleep(0.1) 136 | '; 137 | } 138 | --- udp_listen: 1953 139 | --- udp_reply dns 140 | { 141 | id => 125, 142 | opcode => 0, 143 | qname => 'www.google.com', 144 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 145 | } 146 | --- request 147 | GET /t 148 | --- no_error_log 149 | Attempting to repopulate 'www.google.com' 150 | Repopulating 'www.google.com' 151 | --- response_body 152 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":0}] 153 | 154 | 155 | === TEST 3: Repopulate ignores max_stale 156 | --- http_config eval 157 | "$::HttpConfig" 158 | . q{ 159 | lua_shared_dict dns_cache 1m; 160 | init_by_lua ' 161 | local DNS_Cache = require("resty.dns.cache") 162 | DNS_Cache.init_cache() 163 | '; 164 | } 165 | --- config 166 | location /t { 167 | content_by_lua ' 168 | local DNS_Cache = require("resty.dns.cache") 169 | local dns, err = DNS_Cache.new({ 170 | dict = "dns_cache", 171 | resolver = {nameservers = {{"127.0.0.1", "1953"}}, retrans = 1, timeout = 50 }, 172 | max_stale = 10, 173 | }) 174 | if not dns then 175 | ngx.say(err) 176 | end 177 | dns.resolver._id = 125 178 | dns._debug(true) 179 | 180 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 181 | if not answer then 182 | ngx.say(err) 183 | end 184 | 185 | -- Sleep beyond response TTL 186 | ngx.sleep(1.1) 187 | 188 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 189 | if not answer then 190 | if stale then 191 | answer = stale 192 | else 193 | ngx.say(err) 194 | end 195 | end 196 | 197 | local cjson = require"cjson" 198 | ngx.say(cjson.encode(answer)) 199 | 200 | ngx.sleep(0.1) 201 | '; 202 | } 203 | --- udp_listen: 1953 204 | --- udp_reply dns 205 | { 206 | id => 125, 207 | opcode => 0, 208 | qname => 'www.google.com', 209 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 210 | } 211 | --- request 212 | GET /t 213 | --- error_log 214 | Repopulating 'www.google.com' 215 | Querying: www.google.com 216 | Resolver error 217 | --- response_body 218 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":0}] 219 | 220 | === TEST 4: Multiple queries only trigger 1 repopulate timer 221 | --- http_config eval 222 | "$::HttpConfig" 223 | . q{ 224 | lua_shared_dict dns_cache 1m; 225 | init_by_lua ' 226 | local DNS_Cache = require("resty.dns.cache") 227 | DNS_Cache.init_cache() 228 | '; 229 | } 230 | --- config 231 | location /t { 232 | content_by_lua ' 233 | local DNS_Cache = require("resty.dns.cache") 234 | local dns, err = DNS_Cache.new({ 235 | dict = "dns_cache", 236 | resolver = {nameservers = {{"127.0.0.1", "1953"}}, retrans = 1, timeout = 50 }, 237 | repopulate = true, 238 | }) 239 | if not dns then 240 | ngx.say(err) 241 | end 242 | dns.resolver._id = 125 243 | 244 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 245 | if not answer then 246 | ngx.say(err) 247 | end 248 | dns._debug(true) 249 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 250 | if not answer then 251 | ngx.say(err) 252 | end 253 | local answer, err, stale = dns:query("www.google.com", { qtype = dns.TYPE_A }) 254 | if not answer then 255 | ngx.say(err) 256 | end 257 | 258 | local cjson = require"cjson" 259 | ngx.say(cjson.encode(answer)) 260 | '; 261 | } 262 | --- udp_listen: 1953 263 | --- udp_reply dns 264 | { 265 | id => 125, 266 | opcode => 0, 267 | qname => 'www.google.com', 268 | answer => [{ name => "www.google.com", ipv4 => "127.0.0.1", ttl => 1 }], 269 | } 270 | --- request 271 | GET /t 272 | --- no_error_log 273 | Attempting to repopulate www.google.com 274 | --- response_body 275 | [{"address":"127.0.0.1","type":1,"class":1,"name":"www.google.com","ttl":1}] 276 | -------------------------------------------------------------------------------- /t/TestDNS.pm: -------------------------------------------------------------------------------- 1 | package TestDNS; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use 5.010001; 7 | use Test::Nginx::Socket::Lua -Base; 8 | #use JSON::XS; 9 | 10 | use constant { 11 | TYPE_A => 1, 12 | TYPE_TXT => 16, 13 | TYPE_CNAME => 5, 14 | TYPE_AAAA => 28, 15 | CLASS_INTERNET => 1, 16 | }; 17 | 18 | sub encode_name ($); 19 | sub encode_ipv4 ($); 20 | sub encode_ipv6 ($); 21 | sub gen_dns_reply ($$); 22 | 23 | sub Test::Base::Filter::dns { 24 | my ($self, $code) = @_; 25 | 26 | my $args = $self->current_arguments; 27 | #warn "args: $args"; 28 | if (defined $args && $args ne 'tcp' && $args ne 'udp') { 29 | die "Invalid argument to the \"dns\" filter: $args\n"; 30 | } 31 | 32 | my $mode = $args // 'udp'; 33 | 34 | my $block = $self->current_block; 35 | 36 | my $pointer_spec = $block->dns_pointers; 37 | my @pointers; 38 | if (defined $pointer_spec) { 39 | my @loops = split /\s*,\s*/, $pointer_spec; 40 | for my $loop (@loops) { 41 | my @nodes = split /\s*=>\s*/, $loop; 42 | my $prev; 43 | for my $n (@nodes) { 44 | if ($n !~ /^\d+$/ || $n == 0) { 45 | die "bad name ID in the --- dns_pointers: $n\n"; 46 | } 47 | 48 | if (!defined $prev) { 49 | $prev = $n; 50 | next; 51 | } 52 | 53 | $pointers[$prev] = $n; 54 | } 55 | } 56 | } 57 | 58 | my $input = eval $code; 59 | if ($@) { 60 | die "failed to evaluate code $code: $@\n"; 61 | } 62 | 63 | if (!ref $input) { 64 | return $input; 65 | } 66 | 67 | if (ref $input eq 'ARRAY') { 68 | my @replies; 69 | for my $t (@$input) { 70 | push @replies, gen_dns_reply($t, $mode); 71 | } 72 | 73 | return \@replies; 74 | } 75 | 76 | if (ref $input eq 'HASH') { 77 | return gen_dns_reply($input, $mode); 78 | } 79 | 80 | return $input; 81 | } 82 | 83 | sub gen_dns_reply ($$) { 84 | my ($t, $mode) = @_; 85 | 86 | my @raw_names; 87 | push @raw_names, \($t->{qname}); 88 | 89 | my $answers = $t->{answer} // []; 90 | if (!ref $answers) { 91 | $answers = [$answers]; 92 | } 93 | 94 | for my $ans (@$answers) { 95 | push @raw_names, \($ans->{name}); 96 | if (defined $ans->{cname}) { 97 | push @raw_names, \($ans->{cname}); 98 | } 99 | } 100 | 101 | for my $rname (@raw_names) { 102 | $$rname = encode_name($$rname // ""); 103 | } 104 | 105 | my $qname = $t->{qname}; 106 | 107 | my $s = ''; 108 | 109 | my $id = $t->{id} // 0; 110 | 111 | $s .= pack("n", $id); 112 | #warn "id: ", length($s), " ", encode_json([$s]); 113 | 114 | my $qr = $t->{qr} // 1; 115 | 116 | my $opcode = $t->{opcode} // 0; 117 | 118 | my $aa = $t->{aa} // 0; 119 | 120 | my $tc = $t->{tc} // 0; 121 | my $rd = $t->{rd} // 1; 122 | my $ra = $t->{ra} // 1; 123 | my $rcode = $t->{rcode} // 0; 124 | 125 | my $flags = ($qr << 15) + ($opcode << 11) + ($aa << 10) + ($tc << 9) + ($rd << 8) + ($ra << 7) + $rcode; 126 | #warn sprintf("flags: %b", $flags); 127 | 128 | $flags = pack("n", $flags); 129 | $s .= $flags; 130 | 131 | #warn "flags: ", length($flags), " ", encode_json([$flags]); 132 | 133 | my $qdcount = $t->{qdcount} // 1; 134 | my $ancount = $t->{ancount} // scalar @$answers; 135 | my $nscount = 0; 136 | my $arcount = 0; 137 | 138 | $s .= pack("nnnn", $qdcount, $ancount, $nscount, $arcount); 139 | 140 | #warn "qname: ", length($qname), " ", encode_json([$qname]); 141 | 142 | $s .= $qname; 143 | 144 | my $qs_type = $t->{qtype} // TYPE_A; 145 | my $qs_class = $t->{qclass} // CLASS_INTERNET; 146 | 147 | $s .= pack("nn", $qs_type, $qs_class); 148 | 149 | for my $ans (@$answers) { 150 | my $name = $ans->{name}; 151 | my $type = $ans->{type}; 152 | my $class = $ans->{class}; 153 | my $ttl = $ans->{ttl}; 154 | my $rdlength = $ans->{rdlength}; 155 | my $rddata = $ans->{rddata}; 156 | 157 | my $ipv4 = $ans->{ipv4}; 158 | if (defined $ipv4) { 159 | my ($data, $len) = encode_ipv4($ipv4); 160 | $rddata //= $data; 161 | $rdlength //= $len; 162 | $type //= TYPE_A; 163 | $class //= CLASS_INTERNET; 164 | } 165 | 166 | my $ipv6 = $ans->{ipv6}; 167 | if (defined $ipv6) { 168 | my ($data, $len) = encode_ipv6($ipv6); 169 | $rddata //= $data; 170 | $rdlength //= $len; 171 | $type //= TYPE_AAAA; 172 | $class //= CLASS_INTERNET; 173 | } 174 | 175 | my $cname = $ans->{cname}; 176 | if (defined $cname) { 177 | $rddata //= $cname; 178 | $rdlength //= length $rddata; 179 | $type //= TYPE_CNAME; 180 | $class //= CLASS_INTERNET; 181 | } 182 | 183 | my $txt = $ans->{txt}; 184 | if (defined $txt) { 185 | $rddata //= $txt; 186 | $rdlength //= length $rddata; 187 | $type //= TYPE_TXT; 188 | $class //= CLASS_INTERNET; 189 | } 190 | 191 | $type //= 0; 192 | $class //= 0; 193 | $ttl //= 0; 194 | 195 | #warn "rdlength: $rdlength, rddata: ", encode_json([$rddata]), "\n"; 196 | 197 | $s .= $name . pack("nnNn", $type, $class, $ttl, $rdlength) . $rddata; 198 | } 199 | 200 | if ($mode eq 'tcp') { 201 | return pack("n", length($s)) . $s; 202 | } 203 | 204 | return $s; 205 | } 206 | 207 | sub encode_ipv4 ($) { 208 | my $txt = shift; 209 | my @bytes = split /\./, $txt; 210 | return pack("CCCC", @bytes), 4; 211 | } 212 | 213 | sub encode_ipv6 ($) { 214 | my $txt = shift; 215 | my @groups = split /:/, $txt; 216 | my $nils = 0; 217 | my $nonnils = 0; 218 | for my $g (@groups) { 219 | if ($g eq '') { 220 | $nils++; 221 | } else { 222 | $nonnils++; 223 | $g = hex($g); 224 | } 225 | } 226 | 227 | my $total = $nils + $nonnils; 228 | if ($total > 8 ) { 229 | die "Invalid IPv6 address: too many groups: $total: $txt"; 230 | } 231 | 232 | if ($nils) { 233 | my $found = 0; 234 | my @new_groups; 235 | for my $g (@groups) { 236 | if ($g eq '') { 237 | if ($found) { 238 | next; 239 | } 240 | 241 | for (1 .. 8 - $nonnils) { 242 | push @new_groups, 0; 243 | } 244 | 245 | $found = 1; 246 | 247 | } else { 248 | push @new_groups, $g; 249 | } 250 | } 251 | 252 | @groups = @new_groups; 253 | } 254 | 255 | if (@groups != 8) { 256 | die "Invalid IPv6 address: $txt: @groups\n"; 257 | } 258 | 259 | #warn "IPv6 groups: @groups"; 260 | 261 | return pack("nnnnnnnn", @groups), 16; 262 | } 263 | 264 | sub encode_name ($) { 265 | my $name = shift; 266 | $name =~ s/([^.]+)\.?/chr(length($1)) . $1/ge; 267 | $name .= "\0"; 268 | return $name; 269 | } 270 | 271 | 1 272 | -------------------------------------------------------------------------------- /util/lua-releng.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | sub file_contains ($$); 7 | 8 | my $version; 9 | for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) { 10 | 11 | 12 | print "Checking use of Lua global variables in file $file ...\n"; 13 | system("luac -p -l $file | grep ETGLOBAL | grep -vE 'require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|rawget|rawset|rawlen'"); 14 | file_contains($file, "attempt to write to undeclared variable"); 15 | #system("grep -H -n -E --color '.{81}' $file"); 16 | } 17 | 18 | sub file_contains ($$) { 19 | my ($file, $regex) = @_; 20 | open my $in, $file 21 | or die "Cannot open $file fo reading: $!\n"; 22 | my $content = do { local $/; <$in> }; 23 | close $in; 24 | #print "$content"; 25 | return scalar ($content =~ /$regex/); 26 | } 27 | 28 | if (-d 't') { 29 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 30 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 31 | } 32 | } 33 | --------------------------------------------------------------------------------