├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.markdown ├── dist.ini ├── docker-compose.yml ├── lib └── resolver │ ├── client.lua │ └── master.lua ├── lua-resty-resolver-0.05-1.rockspec └── t ├── client.t ├── master.t └── version.t /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Unit test 11 | run: docker-compose up 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | go 5 | t/servroot/ 6 | reindex 7 | *.src.rock 8 | lua-resty-resolver*.tar.gz -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:stretch-fat 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update && apt-get install -y make cpanminus && cpanm -n Test::Nginx && apt-get clean 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This module is licensed under the BSD license. 2 | Copyright (C) 2012-2020, Thought Foundry Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /opt/openresty 2 | LUA_LIB_DIR ?= $(PREFIX)/lualib 3 | INSTALL ?= install 4 | DNS_SERVER_IP ?= 8.8.8.8 5 | 6 | .PHONY: all test install 7 | 8 | all: ; 9 | 10 | install: all 11 | $(INSTALL) -d $(LUA_LIB_DIR)/resolver 12 | $(INSTALL) lib/resolver/*.lua $(LUA_LIB_DIR)/resolver 13 | 14 | test: install 15 | PATH=$(PREFIX)/nginx/sbin:$$PATH LUA_PATH="$(LUA_PATH);$(PREFIX)/lualib/?.lua;$(PREFIX)/nginx/lualib/?.lua;" DNS_SERVER_IP=$(DNS_SERVER_IP) prove -I../test-nginx/lib -r t 16 | 17 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-resolver - Caching DNS resolver for ngx_lua and LuaJIT 5 | 6 | 7 | Table of Contents 8 | ================= 9 | 10 | * [Name](#name) 11 | * [Status](#status) 12 | * [Description](#description) 13 | * [Motivation](#motivation) 14 | * [Synopsis](#synopsis) 15 | * [Master Methods](#master-methods) 16 | * [new](#master-new) 17 | * [init](#master-init) 18 | * [set](#master-set) 19 | * [client](#master-client) 20 | * [Client Methods](#client-methods) 21 | * [new](#client-new) 22 | * [get](#client-get) 23 | * [Prerequisites](#prerequisites) 24 | * [Installation](#installation) 25 | * [Demo](#demo) 26 | * [Copyright and License](#copyright-and-license) 27 | * [See Also](#see-also) 28 | 29 | 30 | Status 31 | ====== 32 | 33 | This library is still under active development and is considered production ready. 34 | 35 | 36 | Description 37 | =========== 38 | 39 | A pure lua DNS resolver that supports: 40 | 41 | * Caching DNS lookups according to upstream TTL values 42 | * Caching DNS lookups directly from the master (i.e. don't replicate DNS queries per worker thread) 43 | * Support for DNS-based round-robin load balancing (e.g. multiple A records for a single domain) 44 | * Low cache contention via local worker cache (i.e. workers sync from the master using a randomized delay to avoid contention) 45 | * Optional stale results to smooth over DNS availability issues 46 | * Configurable min / max TTL values 47 | * Sensible security (e.g. don't allow potentially harmful results such as `127.0.0.1`) 48 | 49 | 50 | Motivation 51 | ========== 52 | 53 | Q: Why would you want to use this library? 54 | 55 | A: You want dynamic DNS resolution [like they have in Nginx Plus](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolve) 56 | 57 | As of nginx v1.9.x you'll need a commercial license to dynamically resolve upstream server names. The opensource version doesn't support this feature and will simply resolve each domain once and cache it forever (i.e. until nginx restart). 58 | There are [workarounds](https://forum.nginx.org/read.php?2,248924,248924#msg-248924) but they are less than ideal (e.g. don't support [keepalive](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive)). 59 | 60 | This module allows us to use the standard, open source [OpenResty bundle](https://openresty.org) _and_ get the benefits of dynamic upstream name resolution without sacrificing features like keepalive. 61 | 62 | 63 | Synopsis 64 | ======== 65 | 66 | ```lua 67 | # nginx.conf: 68 | 69 | http { 70 | lua_package_path "/path/to/lua-resty-resolver/lib/?.lua;;"; 71 | 72 | # global dns cache (may be shared for multiple domains but for best perf use separate zone per domain) 73 | lua_shared_dict dns_cache 1m; 74 | 75 | # create a global master which caches DNS answers according to upstream TTL 76 | init_by_lua_block { 77 | local err 78 | cdnjs_master, err = require("resolver.master"):new("dns_cache", "cdnjs.cloudflare.com", {"8.8.8.8"}) 79 | if not cdnjs_master then 80 | error("failed to create cdnjs resolver master: " .. err) 81 | end 82 | } 83 | 84 | # create a per-worker client that periodically syncs from the master cache (again, according to TTL values) 85 | init_worker_by_lua_block { 86 | cdnjs_master:init() -- master `init` must be called from a worker since it uses `ngx.timer.at`, it is ok to call multiple times 87 | local err 88 | cdnjs_client, err = cdnjs_master:client() 89 | if not cdnjs_client then 90 | error("failed to create cdnjs resolver client: " .. err) 91 | end 92 | } 93 | 94 | # use per-worker client to lookup host address 95 | upstream cdnjs_backend { 96 | server 0.0.0.1; # just an invalid address as a place holder 97 | 98 | balancer_by_lua_block { 99 | -- note: if `lua_code_cache` is `off` then you'll need to uncomment the next line 100 | -- local cdnjs_client = require("resolver.client"):new("dns_cache", "cdnjs.cloudflare.com") 101 | local address, err = cdnjs_client:get(true) 102 | if not address then 103 | ngx.log(ngx.ERR, "failed to lookup address for cdnjs: ", err) 104 | return ngx.exit(500) 105 | end 106 | 107 | local ok, err = require("ngx.balancer").set_current_peer(address, 80) 108 | if not ok then 109 | ngx.log(ngx.ERR, "failed to set the current peer for cdnjs: ", err) 110 | return ngx.exit(500) 111 | end 112 | } 113 | 114 | keepalive 10; # connection pool MUST come after balancer_by_lua_block 115 | } 116 | 117 | server { 118 | location =/status { 119 | content_by_lua_block { 120 | local address, err = cdnjs_client:get() 121 | if address then 122 | ngx.say("OK") 123 | else 124 | ngx.say(err) 125 | ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) 126 | end 127 | } 128 | } 129 | 130 | location = /js { 131 | proxy_pass http://cdnjs_backend/; 132 | proxy_pass_header Server; 133 | proxy_http_version 1.1; 134 | proxy_set_header Connection ""; 135 | proxy_set_header Host cdnjs.cloudflare.com; 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | [Back to TOC](#table-of-contents) 142 | 143 | 144 | Master Methods 145 | ============== 146 | 147 | To load the master library, 148 | 149 | 1. you need to specify this library's path in ngx_lua's [lua_package_path](https://github.com/openresty/lua-nginx-module#lua_package_path) directive. For example, `lua_package_path "/path/to/lua-resty-resolver/lib/?.lua;;";`. 150 | 2. you use `require` to load the library into a local Lua variable: 151 | 152 | ```lua 153 | local resolver_master = require "resolver.master" 154 | ``` 155 | 156 | 157 | master new 158 | ---------- 159 | `syntax: local master, err = resolver_master:new(shared_dict_key, domain, nameservers [, min_ttl, max_ttl, dns_timeout, blacklist])` 160 | 161 | Creates a new master instance. Returns the instance and an error string. 162 | If successful the error string will be `nil`. 163 | If failed, the the instance will be `nil` and the error string will be populated. 164 | 165 | The `shared_dict_key` argument specifies the [ngx.shared](https://github.com/openresty/lua-nginx-module#ngxshareddict) key to use for cached DNS values. 166 | It is OK to use the same shared dict for multiple domains (i.e. masters are careful not to interfere with other masters resolving other domains). 167 | However, for best performance it is recommended to use a different shared dict for each domain. 168 | 169 | The `domain` argument specifies the fully qualified domain name to resolve. 170 | 171 | The `nameservers` argument specifies the list of nameservers to query (see the `nameservers` param in of [resty.dns.resolver:new](https://github.com/openresty/lua-resty-dns#new). 172 | 173 | The `min_ttl` argument specifies the minimum allowable time in seconds to cache the results of a DNS query. 174 | The default value is `10`. 175 | 176 | The `max_ttl` argument specifies the maximum allowable time in seconds to cache the results of a DNS query. 177 | The default value is `3600` (1 hour). 178 | 179 | The `dns_timeout` argument determines the maximum amount of time in seconds to wait for DNS results. 180 | We also use this value to schedule future DNS queries (i.e. query a bit earlier than the TTL suggests to allow for potential lag up to the `dns_timeout` value when receiving the results). 181 | **Note:** this is _NOT_ the total timeout for all nameservers. The total time is calculated as `dns_timeout * 5` since we use the default `retrans` value in [resty.dns.resolver:new](https://github.com/openresty/lua-resty-dns#new). 182 | The default value is `2`. 183 | 184 | The `blacklist` argument specifies table of banned IP addresses which are ignored if included in DNS server response. 185 | The default value is `{"127.0.0.1"}`. 186 | 187 | The master instance is thread safe and can be safely shared globally (typically declared as a global in a [init_by_lua_block](https://github.com/openresty/lua-nginx-module#init_by_lua_block)). 188 | 189 | [Back to TOC](#table-of-contents) 190 | 191 | 192 | master init 193 | ----------- 194 | `syntax: local ok, err = master:init()` 195 | 196 | Initializes a master instance and causes the master to populate the cache ASAP. Returns a success indicator and an error string. 197 | If successful the success indicator will be truthy and the error string will be `nil`. 198 | If failed, the success indicator will be falsy and the error string will be populated. 199 | 200 | The `init` method **MUST** be called in a context that supports the use of [ngx.timer.at](https://github.com/openresty/lua-nginx-module#ngxtimerat) (e.g. the first usable entrypoint is [init_worker_by_lua_block](https://github.com/openresty/lua-nginx-module#init_worker_by_lua_block)). 201 | This method is idempotent and can be safely called any number of times without any impact. 202 | 203 | [Back to TOC](#table-of-contents) 204 | 205 | 206 | master set 207 | ---------- 208 | `syntax: local next_query_delay = master:set(lookup_result [, exp_offset])` 209 | 210 | Caches the DNS query results. Returns the amount of time in seconds to delay before querying again. 211 | 212 | The `lookup_result` argument is a table containing successful query results (i.e. each entry is a table with IPv4 `address` and `ttl` seconds keys). 213 | 214 | The `exp_offset` argument is the expiration offset to use with each record's TTL (useful for testing). 215 | The default value is the current timestamp determined by [ngx.now](https://github.com/openresty/lua-nginx-module#ngxnow) 216 | 217 | This method should not be used in normal operation and is only really useful for testing. 218 | 219 | [Back to TOC](#table-of-contents) 220 | 221 | 222 | master client 223 | ---------- 224 | `syntax: local client, err = master:client()` 225 | 226 | Convenience method for creating a new client. 227 | Exactly the same as calling [resolver_client:new](#client-new) with the same `shared_dict_key` and `domain` used by the master instance. 228 | 229 | [Back to TOC](#table-of-contents) 230 | 231 | 232 | Client Methods 233 | ============== 234 | 235 | To load the client library, 236 | 237 | 1. you need to specify this library's path in ngx_lua's [lua_package_path](https://github.com/openresty/lua-nginx-module#lua_package_path) directive. For example, `lua_package_path "/path/to/lua-resty-resolver/lib/?.lua;;";`. 238 | 2. you use `require` to load the library into a local Lua variable: 239 | 240 | ```lua 241 | local resolver_client = require "resolver.client" 242 | ``` 243 | 244 | [Back to TOC](#table-of-contents) 245 | 246 | 247 | client new 248 | ---------- 249 | `syntax: local client, err = resolver_client:new(shared_dict_key, domain)` 250 | 251 | Creates a new client instance. Returns the instance and an error string. 252 | If successful the error string will be `nil`. 253 | If failed, the the instance will be `nil` and the error string will be populated. 254 | 255 | The `shared_dict_key` argument specifies the [ngx.shared](https://github.com/openresty/lua-nginx-module#ngxshareddict) key to use for cached DNS values and should be the same value used when creating the master instance. 256 | 257 | The `domain` argument specifies the fully qualified domain name to resolve and should be the same value used when creating the master instance. 258 | 259 | Client instances are _NOT_ thread safe and should be shared only at the worker level (typically declared as a global in a [init_worker_by_lua_block](https://github.com/openresty/lua-nginx-module#init_worker_by_lua_block)). 260 | **Note:** when [lua_code_cache](https://github.com/openresty/lua-nginx-module#lua_code_cache) is `off` (e.g. during development) it is not possible to use a global, per-worker client due to the new lua VM per-request model. 261 | 262 | [Back to TOC](#table-of-contents) 263 | 264 | 265 | client get 266 | ---------- 267 | `syntax: local address, err = client:get([exp_fallback_ok])` 268 | 269 | Retrieve the next cached address using a simple round-robin algorithm to choose when multiple addresses are available. Returns an IPv4 address string and an error string. 270 | If successful the error string will be `nil`. 271 | If failed, the the address will be `nil` and the error string will be populated. 272 | 273 | The `exp_fallback_ok` argument is a boolean which determines if it is OK to return a stale value (`true` when a stale value is allowable, `false` when it is NOT ok to return a stale value). 274 | A stale value is one that has been cached longer than the TTL duration. If there is even one fresh record it will always be returned, even when `exp_fallback_ok` is `true`. 275 | The default value is `false`. 276 | 277 | Clients will automatically sync from the master cache as needed. 278 | Under normal conditions there should never be a situation where the client has no fresh records. However, if the upstream nameserver becomes unavailable the local cache may expire while the master continues to retry. 279 | The client will always retain at least one stale record so that it may continue to service requests until the upstream nameserver becomes available. 280 | It may be preferable to return an error rather than use stale results which is why the `exp_fallback_ok` option defaults to `false`. 281 | 282 | [Back to TOC](#table-of-contents) 283 | 284 | 285 | Prerequisites 286 | ============= 287 | 288 | * [LuaJIT](http://luajit.org) 2.0+ 289 | * [ngx_lua module](http://wiki.nginx.org/HttpLuaModule) 290 | * [lua-resty-dns](https://github.com/openresty/lua-resty-dns) 291 | 292 | These all come with the standard [OpenResty bundle](http://openresty.org). 293 | 294 | [Back to TOC](#table-of-contents) 295 | 296 | 297 | Installation 298 | ============ 299 | 300 | It is recommended to use the latest [OpenResty bundle](http://openresty.org) directly. You'll need to enable LuaJIT when building your ngx_openresty 301 | bundle by passing the `--with-luajit` option to its `./configure` script. 302 | 303 | Also, you'll need to configure the [lua_package_path](https://github.com/openresty/lua-nginx-module#lua_package_path) directive to 304 | add the path of your lua-resty-resolver source tree to ngx_lua's Lua module search path, as in 305 | 306 | ```nginx 307 | # nginx.conf 308 | http { 309 | lua_package_path "/path/to/lua-resty-resolver/lib/?.lua;;"; 310 | ... 311 | } 312 | ``` 313 | 314 | and then load the library in Lua: 315 | 316 | ```lua 317 | local resolver_master = require "resolver.master" 318 | ``` 319 | 320 | Demo 321 | ==== 322 | 323 | A demo app is provided through [this repository](https://github.com/mauricioabreu/dynamic-dns-resolver-demo) 324 | 325 | With this app you can test all features provided by this library without having to integrate it in your real project. 326 | 327 | [Back to TOC](#table-of-contents) 328 | 329 | 330 | Copyright and License 331 | ===================== 332 | 333 | This module is licensed under the BSD license. 334 | 335 | Copyright (C) 2012-2020, Thought Foundry Inc. 336 | 337 | All rights reserved. 338 | 339 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 340 | 341 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 342 | 343 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 344 | 345 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 346 | 347 | [Back to TOC](#table-of-contents) 348 | 349 | 350 | See Also 351 | ======== 352 | * the ngx_lua module: http://wiki.nginx.org/HttpLuaModule 353 | 354 | [Back to TOC](#table-of-contents) 355 | 356 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | # distribution config for opm packaging 2 | name = lua-resty-resolver 3 | abstract = Caching DNS resolver for ngx_lua and LuaJIT 4 | author = Jon Keys 5 | is_original = yes 6 | license = 2bsd 7 | lib_dir = lib 8 | repo_link = https://github.com/jkeys089/lua-resty-resolver 9 | main_module = lib/resolver/master.lua 10 | requires = luajit >= 2.0.0, openresty/lua-resty-dns >= 0.21 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | lua-resty-resolver: 5 | build: 6 | context: . 7 | volumes: 8 | - ".:/app" 9 | image: lua-resty-resolver:latest 10 | command: make test -------------------------------------------------------------------------------- /lib/resolver/client.lua: -------------------------------------------------------------------------------- 1 | local ngx = require "ngx" 2 | local setmetatable = setmetatable 3 | 4 | 5 | local _M = { _VERSION = '0.05' } 6 | 7 | local mt = { __index = _M } 8 | 9 | 10 | -- reverse sort (i.e. descending order) 11 | local function revsort(a, b) 12 | return a > b 13 | end 14 | 15 | 16 | -- sync from master 17 | local function sync(worker) 18 | local hosts = {} 19 | local domain = worker._domain 20 | local domain_len = worker._domain_len 21 | local address_start = worker._address_start 22 | local cache = ngx.shared[worker._shared_key] 23 | local next_sync = 2147483647 24 | 25 | cache:flush_expired() 26 | 27 | for i, k in pairs(cache:get_keys(0)) do 28 | if k:sub(1, domain_len) == domain then 29 | local exp, err = cache:get(k) 30 | if exp then 31 | if exp < next_sync then 32 | next_sync = exp 33 | end 34 | hosts[#hosts+1] = { 35 | address = k:sub(address_start), 36 | exp = exp 37 | } 38 | end 39 | end 40 | end 41 | 42 | if #hosts > 0 then 43 | if next_sync >= 1 then 44 | next_sync = next_sync - 1 45 | end 46 | worker._hosts = hosts 47 | worker._next_sync = next_sync + math.random() -- try not to have all workers attempting to sync at the exact same moment 48 | end 49 | end 50 | 51 | 52 | function _M.new(class, shared_dict_key, domain) 53 | if not shared_dict_key or not ngx.shared[shared_dict_key] then 54 | return nil, "missing shared_dict_key" 55 | end 56 | 57 | local domain = (domain or ""):match("^%s*(.*%S)") 58 | if not domain then 59 | return nil, "missing domain" 60 | end 61 | 62 | local self = setmetatable({ 63 | _shared_key = shared_dict_key, 64 | _domain = domain, 65 | _domain_pref = domain .. "_", 66 | _domain_len = domain:len(), 67 | _address_start = domain:len() + 2, 68 | _hosts = {}, 69 | _next_idx = 1, 70 | _next_sync = 0 71 | }, mt) 72 | 73 | return self, nil 74 | end 75 | 76 | 77 | function _M.get(self, exp_fallback_ok) 78 | local hosts = self._hosts 79 | local tot = #hosts 80 | local now = ngx.now() 81 | 82 | -- re-sync only if necessary 83 | if tot < 1 or now > self._next_sync then 84 | sync(self) 85 | hosts = self._hosts 86 | tot = #hosts 87 | end 88 | 89 | if tot < 1 then 90 | return nil, "no hosts available" 91 | end 92 | 93 | local fallback_idx, host, address, err 94 | local idx = self._next_idx 95 | local exp_idxs = {} 96 | local cnt = 0 97 | 98 | while not host and cnt < tot do 99 | if idx > tot then 100 | idx = 1 101 | end 102 | 103 | local cur_host = hosts[idx] 104 | 105 | if cur_host.exp > now then 106 | host = cur_host 107 | else 108 | if not fallback_idx or cur_host.exp > hosts[fallback_idx].exp then 109 | fallback_idx = idx 110 | end 111 | 112 | exp_idxs[#exp_idxs+1] = idx 113 | 114 | idx = idx + 1 115 | cnt = cnt + 1 116 | end 117 | end 118 | 119 | if host then 120 | address = host.address 121 | self._next_idx = idx + 1 122 | elseif exp_fallback_ok then 123 | address = hosts[fallback_idx].address 124 | else 125 | err = "all hosts expired" 126 | end 127 | 128 | -- remove expired hosts leaving just one fallback 129 | if #exp_idxs > 1 then 130 | table.sort(exp_idxs, revsort) 131 | for i, eidx in ipairs(exp_idxs) do 132 | if eidx ~= fallback_idx then 133 | table.remove(hosts, eidx) 134 | end 135 | end 136 | end 137 | 138 | return address, err 139 | end 140 | 141 | 142 | return _M 143 | -------------------------------------------------------------------------------- /lib/resolver/master.lua: -------------------------------------------------------------------------------- 1 | local ngx = require "ngx" 2 | local client = require "resolver.client" 3 | local resolver = require "resty.dns.resolver" 4 | local setmetatable = setmetatable 5 | 6 | 7 | local _M = { _VERSION = '0.05' } 8 | 9 | local mt = { __index = _M } 10 | 11 | local resolve, schedule 12 | 13 | schedule = function(master, delay) 14 | local ok, err = ngx.timer.at(delay, function(premature, master) 15 | if not premature then 16 | resolve(master) 17 | end 18 | end, master) 19 | 20 | if not ok then 21 | ngx.log(ngx.CRIT, "resolver master failed to create resolve timer for domain '", master._domain, "': ", err) 22 | end 23 | end 24 | 25 | 26 | resolve = function(master) 27 | local res, err = resolver:new({ 28 | nameservers = master._nameservers, 29 | timeout = master._timeout * 1000 30 | }) 31 | if not res then 32 | ngx.log(ngx.CRIT, "resolver master failed to create resty.dns.resolver for domain '", master._domain, "': ", err) 33 | return 34 | end 35 | 36 | local domain = master._domain 37 | local ns_cnt = #master._nameservers 38 | 39 | local answers, err 40 | 41 | while ns_cnt ~= 0 do 42 | local tries = {} 43 | answers, err = res:query(domain, master._qopts, tries) 44 | 45 | for i, err in ipairs(tries) do 46 | err = "resolver master failed to query DNS for domain '" .. domain .. "': " .. err 47 | ngx.log(ngx.DEBUG, err) 48 | end 49 | 50 | if not answers then 51 | -- unable to even send the query, move along 52 | err = "resolver master failed to query DNS for domain '" .. domain .. "': " .. err 53 | ngx.log(ngx.DEBUG, err) 54 | ns_cnt = ns_cnt - 1 55 | else 56 | if answers.errcode then 57 | -- executed the query but got a bad result, move along 58 | err = "resolver master failed to resolve domain '" .. domain .. "': [" .. answers.errcode .. "] " .. answers.errstr 59 | ngx.log(ngx.DEBUG, err) 60 | ns_cnt = ns_cnt - 1 61 | else 62 | -- we got a usable result! 63 | err = nil 64 | ns_cnt = 0 65 | end 66 | end 67 | end 68 | 69 | if err then 70 | -- exhausted all nameservers and couldn't get a usable result 71 | ngx.log(ngx.ERR, err) 72 | schedule(master, master._min_ttl) 73 | return 74 | end 75 | 76 | -- DNS query was successful, store the result and schedule the next lookup 77 | schedule(master, master:set(answers)) 78 | end 79 | 80 | 81 | function _M.new(class, shared_dict_key, domain, nameservers, min_ttl, max_ttl, dns_timeout, blacklist) 82 | if not shared_dict_key or not ngx.shared[shared_dict_key] then 83 | return nil, "missing shared_dict_key" 84 | end 85 | 86 | local domain = (domain or ""):match("^%s*(.*%S)") 87 | if not domain then 88 | return nil, "missing domain" 89 | end 90 | 91 | if not nameservers or #nameservers < 1 then 92 | return nil, "missing nameservers" 93 | end 94 | 95 | local minttl = min_ttl or 10 96 | if minttl <= 0 then 97 | return nil, "min_ttl must be a positive number" 98 | end 99 | 100 | local maxttl = max_ttl or 3600 101 | if maxttl < minttl then 102 | return nil, "max_ttl must >= min_ttl (" .. minttl .. ")" 103 | end 104 | 105 | local timeout = dns_timeout or 2 106 | if timeout <= 0 then 107 | return nil, "dns_timeout must be a positive number" 108 | end 109 | 110 | blacklist = blacklist or { "127.0.0.1" } 111 | if type(blacklist) ~= 'table' then 112 | return nil, "blacklist must be a table" 113 | end 114 | local blacklist_table = {} 115 | for _, l in ipairs(blacklist) do blacklist_table[l] = true end 116 | 117 | local self = setmetatable({ 118 | _name = "_master_[" .. domain .. "]_", 119 | _shared_key = shared_dict_key, 120 | _domain = domain, 121 | _domain_pref = domain .. "_", 122 | _qopts = { qtype = resolver.TYPE_A }, 123 | _min_ttl = minttl, 124 | _max_ttl = maxttl, 125 | _timeout = timeout, 126 | _nameservers = nameservers, 127 | _blacklist = blacklist_table 128 | }, mt) 129 | 130 | ngx.shared[self._shared_key]:delete(self._name) 131 | 132 | return self, nil 133 | end 134 | 135 | 136 | function _M.init(self) 137 | -- check process type 138 | if ngx.worker.id() == nil then 139 | -- helper process, do not even try to initialize master resolver 140 | return true, nil 141 | end 142 | -- normal process, try to initialize master resolver 143 | local added, err, forced = ngx.shared[self._shared_key]:add(self._name, "_master_") 144 | if added then 145 | return ngx.timer.at(0, function(premature, master) 146 | if not premature then 147 | resolve(master) 148 | end 149 | end, self) 150 | end 151 | return true, nil 152 | end 153 | 154 | 155 | function _M.client(self) 156 | return client:new(self._shared_key, self._domain) 157 | end 158 | 159 | 160 | function _M.set(self, lookup_result, exp_offset) 161 | local prefix = self._domain_pref 162 | local cache = ngx.shared[self._shared_key] 163 | local minttl = self._min_ttl 164 | local maxttl = self._max_ttl 165 | local blacklist = self._blacklist 166 | local timeout = self._timeout 167 | local next_res = maxttl 168 | local exp_offset = exp_offset or ngx.now() 169 | 170 | for i, ans in ipairs(lookup_result) do 171 | if ans.address and not blacklist[ans.address] then 172 | local ttl = ans.ttl 173 | 174 | if ttl < minttl then 175 | ttl = minttl 176 | elseif ttl > maxttl then 177 | ttl = maxttl 178 | end 179 | 180 | -- let the smallest returned TTL determine when we'll query again 181 | if ttl < next_res then 182 | next_res = ttl 183 | end 184 | 185 | local exp = exp_offset + ttl 186 | 187 | cache:set(prefix .. ans.address, exp, ttl) 188 | end 189 | end 190 | 191 | if next_res > timeout then 192 | next_res = next_res - timeout 193 | end 194 | 195 | return next_res 196 | end 197 | 198 | 199 | return _M 200 | -------------------------------------------------------------------------------- /lua-resty-resolver-0.05-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-resolver" 2 | version = "0.05-1" 3 | source = { 4 | url = "git://github.com/jkeys089/lua-resty-resolver", 5 | } 6 | description = { 7 | summary = "Caching DNS resolver for ngx_lua and LuaJIT", 8 | detailed = [[ 9 | A pure lua DNS resolver that supports: 10 | 11 | Caching DNS lookups according to upstream TTL values 12 | Caching DNS lookups directly from the master (i.e. don't replicate DNS queries per worker thread) 13 | Support for DNS-based round-robin load balancing (e.g. multiple A records for a single domain) 14 | Low cache contention via local worker cache (i.e. workers sync from the master using a randomized delay to avoid contention) 15 | Optional stale results to smooth over DNS availability issues 16 | Configurable min / max TTL values 17 | Sensible security (e.g. don't allow potentially harmful results such as 127.0.0.1) 18 | ]], 19 | homepage = "https://github.com/jkeys089/lua-resty-resolver", 20 | license = "MIT" 21 | } 22 | dependencies = { 23 | "lua-resty-dns >= 0.21-1" 24 | } 25 | build = { 26 | type = "builtin", 27 | modules = { 28 | ["resolver.client"] = "lib/resolver/client.lua", 29 | ["resolver.master"] = "lib/resolver/master.lua" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /t/client.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | 3 | workers(2); 4 | 5 | run_tests(); 6 | 7 | __DATA__ 8 | 9 | 10 | === TEST 1: catch shared dict init problems 11 | --- config 12 | location /t { 13 | content_by_lua_block { 14 | local resolver_client = require "resolver.client" 15 | local ok, err = resolver_client:new(nil, "google.com") 16 | ngx.say(err) 17 | ok, err = resolver_client:new("bad_key", "google.com") 18 | ngx.say(err) 19 | } 20 | } 21 | --- request 22 | GET /t 23 | --- response_body 24 | missing shared_dict_key 25 | missing shared_dict_key 26 | --- no_error_log 27 | [error] 28 | 29 | 30 | === TEST 2: catch domain init problems 31 | --- http_config 32 | lua_shared_dict test_res 1m; 33 | --- config 34 | location /t { 35 | content_by_lua_block { 36 | local resolver_client = require "resolver.client" 37 | local ok, err = resolver_client:new("test_res", nil) 38 | ngx.say(err) 39 | ok, err = resolver_client:new("test_res", "") 40 | ngx.say(err) 41 | ok, err = resolver_client:new("test_res", " ") 42 | ngx.say(err) 43 | } 44 | } 45 | --- request 46 | GET /t 47 | --- response_body 48 | missing domain 49 | missing domain 50 | missing domain 51 | --- no_error_log 52 | [error] 53 | 54 | 55 | === TEST 3: catch no hosts error 56 | --- http_config 57 | lua_shared_dict test_res 1m; 58 | --- config 59 | location /t { 60 | content_by_lua_block { 61 | local resolver_client = require "resolver.client" 62 | local goog_client = resolver_client:new("test_res", "google.com") 63 | local ok, err = goog_client:get() 64 | ngx.say(err) 65 | } 66 | } 67 | --- request 68 | GET /t 69 | --- response_body 70 | no hosts available 71 | --- no_error_log 72 | [error] 73 | 74 | 75 | === TEST 4: catch expired hosts error 76 | --- main_config 77 | env DNS_SERVER_IP=8.8.8.8; 78 | --- http_config 79 | lua_shared_dict test_res 1m; 80 | init_by_lua_block { 81 | local resolver_master = require "resolver.master" 82 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 83 | goog_master:set({ 84 | { 85 | address = "17.0.0.1", 86 | ttl = 300 87 | } 88 | }, 1) 89 | } 90 | init_worker_by_lua_block { 91 | local resolver_client = require "resolver.client" 92 | goog_client = resolver_client:new("test_res", "google.com") 93 | } 94 | --- config 95 | location /t { 96 | content_by_lua_block { 97 | local address, err = goog_client:get() 98 | ngx.say(err) 99 | } 100 | } 101 | --- request 102 | GET /t 103 | --- response_body 104 | all hosts expired 105 | --- no_error_log 106 | [error] 107 | 108 | 109 | === TEST 5: get an address 110 | --- main_config 111 | env DNS_SERVER_IP=8.8.8.8; 112 | --- http_config 113 | lua_shared_dict test_res 1m; 114 | init_by_lua_block { 115 | local resolver_master = require "resolver.master" 116 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 117 | goog_master:set({ 118 | { 119 | address = "17.0.0.1", 120 | ttl = 300 121 | } 122 | }) 123 | } 124 | init_worker_by_lua_block { 125 | local resolver_client = require "resolver.client" 126 | goog_client = resolver_client:new("test_res", "google.com") 127 | } 128 | --- config 129 | location /t { 130 | content_by_lua_block { 131 | local address, err = goog_client:get() 132 | ngx.say(address) 133 | } 134 | } 135 | --- request 136 | GET /t 137 | --- response_body 138 | 17.0.0.1 139 | --- no_error_log 140 | [error] 141 | 142 | 143 | === TEST 6: get stale address 144 | --- main_config 145 | env DNS_SERVER_IP=8.8.8.8; 146 | --- http_config 147 | lua_shared_dict test_res 1m; 148 | init_by_lua_block { 149 | local resolver_master = require "resolver.master" 150 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 151 | goog_master:set({ 152 | { 153 | address = "17.0.0.1", 154 | ttl = 300 155 | } 156 | }, 1) 157 | } 158 | init_worker_by_lua_block { 159 | local resolver_client = require "resolver.client" 160 | goog_client = resolver_client:new("test_res", "google.com") 161 | } 162 | --- config 163 | location /t { 164 | content_by_lua_block { 165 | local address, err = goog_client:get(true) 166 | ngx.say(address) 167 | } 168 | } 169 | --- request 170 | GET /t 171 | --- response_body 172 | 17.0.0.1 173 | --- no_error_log 174 | [error] 175 | 176 | 177 | === TEST 7: get round-robin 178 | --- main_config 179 | env DNS_SERVER_IP=8.8.8.8; 180 | --- http_config 181 | lua_shared_dict test_res 1m; 182 | init_by_lua_block { 183 | local resolver_master = require "resolver.master" 184 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 185 | goog_master:set({ 186 | { 187 | address = "17.0.0.1", 188 | ttl = 300 189 | }, 190 | { 191 | address = "17.0.0.2", 192 | ttl = 300 193 | }, 194 | { 195 | address = "17.0.0.3", 196 | ttl = 300 197 | } 198 | }) 199 | } 200 | init_worker_by_lua_block { 201 | local resolver_client = require "resolver.client" 202 | goog_client = resolver_client:new("test_res", "google.com") 203 | } 204 | --- config 205 | location /t { 206 | content_by_lua_block { 207 | local address, err = goog_client:get() 208 | ngx.say(address) 209 | address, err = goog_client:get() 210 | ngx.say(address) 211 | address, err = goog_client:get() 212 | ngx.say(address) 213 | address, err = goog_client:get() 214 | ngx.say(address) 215 | address, err = goog_client:get() 216 | ngx.say(address) 217 | address, err = goog_client:get() 218 | ngx.say(address) 219 | } 220 | } 221 | --- request 222 | GET /t 223 | --- response_body 224 | 17.0.0.1 225 | 17.0.0.2 226 | 17.0.0.3 227 | 17.0.0.1 228 | 17.0.0.2 229 | 17.0.0.3 230 | --- no_error_log 231 | [error] 232 | 233 | 234 | === TEST 8: skip expired 235 | --- main_config 236 | env DNS_SERVER_IP=8.8.8.8; 237 | --- http_config 238 | lua_shared_dict test_res 1m; 239 | init_by_lua_block { 240 | local resolver_master = require "resolver.master" 241 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 242 | local exp_offset = ngx.now() - 30 243 | goog_master:set({ 244 | { 245 | address = "17.0.0.1", 246 | ttl = 10 247 | }, 248 | { 249 | address = "17.0.0.2", 250 | ttl = 10 251 | }, 252 | { 253 | address = "17.0.0.3", 254 | ttl = 300 255 | } 256 | }, exp_offset) 257 | } 258 | init_worker_by_lua_block { 259 | local resolver_client = require "resolver.client" 260 | goog_client = resolver_client:new("test_res", "google.com") 261 | } 262 | --- config 263 | location /t { 264 | content_by_lua_block { 265 | local address, err = goog_client:get() 266 | ngx.say(address) 267 | address, err = goog_client:get(true) 268 | ngx.say(address) 269 | } 270 | } 271 | --- request 272 | GET /t 273 | --- response_body 274 | 17.0.0.3 275 | 17.0.0.3 276 | --- no_error_log 277 | [error] 278 | 279 | 280 | === TEST 9: select least expired 281 | --- main_config 282 | env DNS_SERVER_IP=8.8.8.8; 283 | --- http_config 284 | lua_shared_dict test_res 1m; 285 | init_by_lua_block { 286 | local resolver_master = require "resolver.master" 287 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 288 | local exp_offset = ngx.now() - 30 289 | goog_master:set({ 290 | { 291 | address = "17.0.0.1", 292 | ttl = 20 293 | }, 294 | { 295 | address = "17.0.0.2", 296 | ttl = 25 297 | }, 298 | { 299 | address = "17.0.0.3", 300 | ttl = 15 301 | } 302 | }, exp_offset) 303 | } 304 | init_worker_by_lua_block { 305 | local resolver_client = require "resolver.client" 306 | goog_client = resolver_client:new("test_res", "google.com") 307 | } 308 | --- config 309 | location /t { 310 | content_by_lua_block { 311 | local address, err = goog_client:get(true) 312 | ngx.say(address) 313 | address, err = goog_client:get(true) 314 | ngx.say(address) 315 | } 316 | } 317 | --- request 318 | GET /t 319 | --- response_body 320 | 17.0.0.2 321 | 17.0.0.2 322 | --- no_error_log 323 | [error] 324 | 325 | 326 | === TEST 10: obey ttl for re-sync 327 | --- main_config 328 | env DNS_SERVER_IP=8.8.8.8; 329 | --- http_config 330 | lua_shared_dict test_res 1m; 331 | init_by_lua_block { 332 | local resolver_master = require "resolver.master" 333 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 1) 334 | goog_master:set({ 335 | { 336 | address = "17.0.0.1", 337 | ttl = 2 338 | } 339 | }) 340 | } 341 | init_worker_by_lua_block { 342 | local resolver_client = require "resolver.client" 343 | goog_client = resolver_client:new("test_res", "google.com") 344 | ngx.timer.at(1, function() 345 | goog_master:set({ 346 | { 347 | address = "17.0.0.2", 348 | ttl = 30 349 | } 350 | }) 351 | end) 352 | } 353 | --- config 354 | location /t { 355 | content_by_lua_block { 356 | local address, err = goog_client:get() 357 | ngx.say(address) 358 | ngx.sleep(1) 359 | address, err = goog_client:get() 360 | ngx.say(address) 361 | ngx.sleep(1) 362 | address, err = goog_client:get() 363 | ngx.say(address) 364 | } 365 | } 366 | --- request 367 | GET /t 368 | --- response_body 369 | 17.0.0.1 370 | 17.0.0.1 371 | 17.0.0.2 372 | --- no_error_log 373 | [error] 374 | --- timeout: 3 375 | 376 | 377 | === TEST 11: don't wipe stale local entries on failed re-sync 378 | --- main_config 379 | env DNS_SERVER_IP=8.8.8.8; 380 | --- http_config 381 | lua_shared_dict test_res 1m; 382 | init_by_lua_block { 383 | local resolver_master = require "resolver.master" 384 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 1) 385 | goog_master:set({ 386 | { 387 | address = "17.0.0.1", 388 | ttl = 1 389 | } 390 | }) 391 | } 392 | init_worker_by_lua_block { 393 | local resolver_client = require "resolver.client" 394 | goog_client = resolver_client:new("test_res", "google.com") 395 | ngx.timer.at(1, function() 396 | goog_master:set({ 397 | { 398 | address = "17.0.0.2", 399 | ttl = 1 400 | } 401 | }) 402 | end) 403 | } 404 | --- config 405 | location /t { 406 | content_by_lua_block { 407 | local address, err = goog_client:get() 408 | ngx.say(address) 409 | ngx.sleep(2.5) 410 | address, err = goog_client:get() 411 | ngx.say(address) 412 | address, err = goog_client:get(true) 413 | ngx.say(address) 414 | } 415 | } 416 | --- request 417 | GET /t 418 | --- response_body 419 | 17.0.0.1 420 | nil 421 | 17.0.0.1 422 | --- no_error_log 423 | [error] 424 | --- timeout: 3 425 | 426 | 427 | === TEST 12: sync picks up changes 428 | --- main_config 429 | env DNS_SERVER_IP=8.8.8.8; 430 | --- http_config 431 | lua_shared_dict test_res 1m; 432 | init_by_lua_block { 433 | local resolver_master = require "resolver.master" 434 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 1) 435 | goog_master:set({ 436 | { 437 | address = "17.0.0.1", 438 | ttl = 1 439 | } 440 | }) 441 | } 442 | init_worker_by_lua_block { 443 | local resolver_client = require "resolver.client" 444 | goog_client = resolver_client:new("test_res", "google.com") 445 | ngx.timer.at(1, function() 446 | goog_master:set({ 447 | { 448 | address = "17.0.0.2", 449 | ttl = 1 450 | } 451 | }) 452 | end) 453 | } 454 | --- config 455 | location /t { 456 | content_by_lua_block { 457 | local address, err = goog_client:get() 458 | ngx.say(address) 459 | ngx.sleep(1) 460 | address, err = goog_client:get() 461 | ngx.say(address) 462 | ngx.sleep(1) 463 | address, err = goog_client:get() 464 | ngx.say(address) 465 | address, err = goog_client:get(true) 466 | ngx.say(address) 467 | } 468 | } 469 | --- request 470 | GET /t 471 | --- response_body 472 | 17.0.0.1 473 | 17.0.0.2 474 | nil 475 | 17.0.0.2 476 | --- no_error_log 477 | [error] 478 | --- timeout: 3 479 | 480 | 481 | -------------------------------------------------------------------------------- /t/master.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | 3 | workers(2); 4 | 5 | run_tests(); 6 | 7 | __DATA__ 8 | 9 | 10 | === TEST 1: manual hosts 11 | --- main_config 12 | env DNS_SERVER_IP=8.8.8.8; 13 | --- http_config 14 | lua_shared_dict test_res 1m; 15 | init_by_lua_block { 16 | local resolver_master = require "resolver.master" 17 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 18 | goog_master:set({ 19 | { 20 | address = "17.0.0.1", 21 | ttl = 300 22 | } 23 | }, 1482624000) 24 | } 25 | --- config 26 | location /t { 27 | content_by_lua_block { 28 | local k = ngx.shared.test_res:get_keys() 29 | local v, err = ngx.shared.test_res:get(k[1]) 30 | ngx.say(k[1]) 31 | ngx.say(v) 32 | } 33 | } 34 | --- request 35 | GET /t 36 | --- response_body 37 | google.com_17.0.0.1 38 | 1482624300 39 | --- no_error_log 40 | [error] 41 | 42 | 43 | === TEST 2: query hosts 44 | --- main_config 45 | env DNS_SERVER_IP=8.8.8.8; 46 | --- http_config 47 | lua_shared_dict test_res 1m; 48 | init_by_lua_block { 49 | local resolver_master = require "resolver.master" 50 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 51 | } 52 | init_worker_by_lua_block { 53 | goog_master:init() 54 | } 55 | --- config 56 | location /t { 57 | content_by_lua_block { 58 | ngx.sleep(2) 59 | local keys = ngx.shared.test_res:get_keys() 60 | for i, k in ipairs(keys) do 61 | local v, err = ngx.shared.test_res:get(k) 62 | if v ~= "_master_" then 63 | ngx.say(k) 64 | ngx.say(v) 65 | break 66 | end 67 | end 68 | } 69 | } 70 | --- request 71 | GET /t 72 | --- response_body_like chop 73 | ^google.com_\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*\d+$ 74 | --- no_error_log 75 | [error] 76 | 77 | 78 | === TEST 3: multiple inits single master 79 | --- main_config 80 | env DNS_SERVER_IP=8.8.8.8; 81 | --- http_config 82 | lua_shared_dict test_res 1m; 83 | init_by_lua_block { 84 | local resolver_master = require "resolver.master" 85 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 86 | } 87 | init_worker_by_lua_block { 88 | for i = 1,10 do 89 | goog_master:init() 90 | end 91 | } 92 | --- config 93 | location /t { 94 | content_by_lua_block { 95 | ngx.sleep(2) 96 | local keys = ngx.shared.test_res:get_keys() 97 | for i, k in ipairs(keys) do 98 | local v, err = ngx.shared.test_res:get(k) 99 | if v == "_master_" then 100 | ngx.say(v) 101 | end 102 | end 103 | } 104 | } 105 | --- request 106 | GET /t 107 | --- response_body_like chop 108 | ^_master_$ 109 | --- no_error_log 110 | [error] 111 | 112 | 113 | === TEST 4: multiple inits multiple masters 114 | --- main_config 115 | env DNS_SERVER_IP=8.8.8.8; 116 | --- http_config 117 | lua_shared_dict test_res 1m; 118 | init_by_lua_block { 119 | local resolver_master = require "resolver.master" 120 | goog_master_a = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 121 | goog_master_b = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 122 | goog_master_c = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 123 | } 124 | init_worker_by_lua_block { 125 | for i = 1,10 do 126 | goog_master_a:init() 127 | goog_master_b:init() 128 | goog_master_c:init() 129 | end 130 | } 131 | --- config 132 | location /t { 133 | content_by_lua_block { 134 | ngx.sleep(2) 135 | local keys = ngx.shared.test_res:get_keys() 136 | for i, k in ipairs(keys) do 137 | local v, err = ngx.shared.test_res:get(k) 138 | if v == "_master_" then 139 | ngx.say(v) 140 | end 141 | end 142 | } 143 | } 144 | --- request 145 | GET /t 146 | --- response_body_like chop 147 | ^_master_$ 148 | --- no_error_log 149 | [error] 150 | 151 | 152 | === TEST 5: filter banned ip's 153 | --- main_config 154 | env DNS_SERVER_IP=8.8.8.8; 155 | --- http_config 156 | lua_shared_dict test_res 1m; 157 | init_by_lua_block { 158 | local resolver_master = require "resolver.master" 159 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}) 160 | goog_master:set({ 161 | { 162 | address = "17.0.0.1", 163 | ttl = 300 164 | }, 165 | { 166 | address = "127.0.0.1", -- banned 167 | ttl = 300 168 | }, 169 | }, 1482624000) 170 | } 171 | --- config 172 | location /t { 173 | content_by_lua_block { 174 | local keys = ngx.shared.test_res:get_keys() 175 | for i, k in ipairs(keys) do 176 | local v, err = ngx.shared.test_res:get(k) 177 | if v ~= "_master_" then 178 | ngx.say(k) 179 | ngx.say(v) 180 | end 181 | end 182 | } 183 | } 184 | --- request 185 | GET /t 186 | --- response_body 187 | google.com_17.0.0.1 188 | 1482624300 189 | --- no_error_log 190 | [error] 191 | 192 | 193 | === TEST 6: catch shared dict init problems 194 | --- main_config 195 | env DNS_SERVER_IP=8.8.8.8; 196 | --- http_config 197 | lua_shared_dict test_res 1m; 198 | --- config 199 | location /t { 200 | content_by_lua_block { 201 | local resolver_master = require "resolver.master" 202 | local goog_master, err = resolver_master:new(nil, "google.com", {os.getenv("DNS_SERVER_IP")}) 203 | ngx.say(err) 204 | goog_master, err = resolver_master:new("bad_key", "google.com", {os.getenv("DNS_SERVER_IP")}) 205 | ngx.say(err) 206 | } 207 | } 208 | --- request 209 | GET /t 210 | --- response_body 211 | missing shared_dict_key 212 | missing shared_dict_key 213 | --- no_error_log 214 | [error] 215 | 216 | 217 | === TEST 7: catch domain init problems 218 | --- main_config 219 | env DNS_SERVER_IP=8.8.8.8; 220 | --- http_config 221 | lua_shared_dict test_res 1m; 222 | --- config 223 | location /t { 224 | content_by_lua_block { 225 | local resolver_master = require "resolver.master" 226 | local goog_master, err = resolver_master:new("test_res", nil, {os.getenv("DNS_SERVER_IP")}) 227 | ngx.say(err) 228 | goog_master, err = resolver_master:new("test_res", "", {os.getenv("DNS_SERVER_IP")}) 229 | ngx.say(err) 230 | goog_master, err = resolver_master:new("test_res", " ", {os.getenv("DNS_SERVER_IP")}) 231 | ngx.say(err) 232 | } 233 | } 234 | --- request 235 | GET /t 236 | --- response_body 237 | missing domain 238 | missing domain 239 | missing domain 240 | --- no_error_log 241 | [error] 242 | 243 | 244 | === TEST 8: catch min / max ttl init problems 245 | --- main_config 246 | env DNS_SERVER_IP=8.8.8.8; 247 | --- http_config 248 | lua_shared_dict test_res 1m; 249 | --- config 250 | location /t { 251 | content_by_lua_block { 252 | local resolver_master = require "resolver.master" 253 | local goog_master, err = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 0, 30) 254 | ngx.say(err) 255 | goog_master, err = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, -1, 30) 256 | ngx.say(err) 257 | goog_master, err = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 9) 258 | ngx.say(err) 259 | } 260 | } 261 | --- request 262 | GET /t 263 | --- response_body 264 | min_ttl must be a positive number 265 | min_ttl must be a positive number 266 | max_ttl must >= min_ttl (10) 267 | --- no_error_log 268 | [error] 269 | 270 | 271 | === TEST 9: catch dns_timeout init problems 272 | --- main_config 273 | env DNS_SERVER_IP=8.8.8.8; 274 | --- http_config 275 | lua_shared_dict test_res 1m; 276 | --- config 277 | location /t { 278 | content_by_lua_block { 279 | local resolver_master = require "resolver.master" 280 | local goog_master, err = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30, 0) 281 | ngx.say(err) 282 | goog_master, err = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30, -1) 283 | ngx.say(err) 284 | } 285 | } 286 | --- request 287 | GET /t 288 | --- response_body 289 | dns_timeout must be a positive number 290 | dns_timeout must be a positive number 291 | --- no_error_log 292 | [error] 293 | 294 | 295 | === TEST 10: catch nameserver init problems 296 | --- http_config 297 | lua_shared_dict test_res 1m; 298 | --- config 299 | location /t { 300 | content_by_lua_block { 301 | local resolver_master = require "resolver.master" 302 | local goog_master, err = resolver_master:new("test_res", "google.com", nil, 10, 30) 303 | ngx.say(err) 304 | goog_master, err = resolver_master:new("test_res", "google.com", {}, 10, 30) 305 | ngx.say(err) 306 | } 307 | } 308 | --- request 309 | GET /t 310 | --- response_body 311 | missing nameservers 312 | missing nameservers 313 | --- no_error_log 314 | [error] 315 | 316 | 317 | === TEST 11: bogus nameserver alerts 318 | --- http_config 319 | lua_shared_dict test_res 1m; 320 | init_by_lua_block { 321 | local resolver_master = require "resolver.master" 322 | goog_master = resolver_master:new("test_res", "google.com", {"127.0.0.1"}, 10, 30) 323 | } 324 | init_worker_by_lua_block { 325 | goog_master:init() 326 | } 327 | --- config 328 | location /t { 329 | echo "OK"; 330 | } 331 | --- request 332 | GET /t 333 | --- wait: 2 334 | --- error_log 335 | resolver master failed to query DNS for domain 'google.com' 336 | 337 | 338 | === TEST 12: bogus nameserver struct causes resolver init to alert 339 | --- http_config 340 | lua_shared_dict test_res 1m; 341 | init_by_lua_block { 342 | local resolver_master = require "resolver.master" 343 | goog_master = resolver_master:new("test_res", "google.com", {{"boom", 0}}, 10, 30) 344 | } 345 | init_worker_by_lua_block { 346 | goog_master:init() 347 | } 348 | --- config 349 | location /t { 350 | echo "OK"; 351 | } 352 | --- request 353 | GET /t 354 | --- wait: 2 355 | --- error_log 356 | resolver master failed to create resty.dns.resolver for domain 'google.com' 357 | 358 | 359 | === TEST 13: backup nameserver 360 | --- main_config 361 | env DNS_SERVER_IP=8.8.8.8; 362 | --- http_config 363 | lua_shared_dict test_res 1m; 364 | init_by_lua_block { 365 | local resolver_master = require "resolver.master" 366 | goog_master = resolver_master:new("test_res", "google.com", {"127.0.0.1",os.getenv("DNS_SERVER_IP")}, 1, 1) 367 | } 368 | init_worker_by_lua_block { 369 | goog_master:init() 370 | } 371 | --- config 372 | location /t { 373 | content_by_lua_block { 374 | ngx.sleep(10) 375 | local keys = ngx.shared.test_res:get_keys() 376 | for i, k in ipairs(keys) do 377 | local v, err = ngx.shared.test_res:get_stale(k) 378 | if v ~= "_master_" then 379 | ngx.say(k) 380 | ngx.say(v) 381 | break 382 | end 383 | end 384 | } 385 | } 386 | --- request 387 | GET /t 388 | --- response_body_like chop 389 | ^google.com_\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*\d+$ 390 | --- error_log 391 | resolver master failed to query DNS for domain 'google.com' 392 | --- timeout: 11 393 | 394 | 395 | === TEST 14: bogus domain alerts 396 | --- main_config 397 | env DNS_SERVER_IP=8.8.8.8; 398 | --- http_config 399 | lua_shared_dict test_res 1m; 400 | init_by_lua_block { 401 | local resolver_master = require "resolver.master" 402 | goog_master = resolver_master:new("test_res", "iam.notreal.fake", {os.getenv("DNS_SERVER_IP")}, 10, 30) 403 | } 404 | init_worker_by_lua_block { 405 | goog_master:init() 406 | } 407 | --- config 408 | location /t { 409 | echo "OK"; 410 | } 411 | --- request 412 | GET /t 413 | --- wait: 2 414 | --- error_log 415 | resolver master failed to resolve domain 'iam.notreal.fake' 416 | 417 | 418 | === TEST 15: failed init 419 | --- main_config 420 | env DNS_SERVER_IP=8.8.8.8; 421 | --- http_config 422 | lua_max_pending_timers 1; 423 | lua_shared_dict test_res 1m; 424 | init_by_lua_block { 425 | local resolver_master = require "resolver.master" 426 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 427 | } 428 | init_worker_by_lua_block { 429 | ngx.timer.at(30, function() return true end) 430 | local ok, err = goog_master:init() 431 | if not ok then 432 | ngx.log(ngx.ERR, err) 433 | end 434 | } 435 | --- config 436 | location /t { 437 | echo "OK"; 438 | } 439 | --- request 440 | GET /t 441 | --- error_log 442 | too many pending timers 443 | 444 | 445 | === TEST 16: unable to schedule resolve alerts 446 | --- main_config 447 | env DNS_SERVER_IP=8.8.8.8; 448 | --- http_config 449 | lua_max_pending_timers 2; 450 | lua_shared_dict test_res 1m; 451 | init_by_lua_block { 452 | local resolver_master = require "resolver.master" 453 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 2, 2) 454 | } 455 | init_worker_by_lua_block { 456 | ngx.timer.at(0, function() 457 | ngx.timer.at(0, function() 458 | ngx.timer.at(30, function() return true end) 459 | ngx.timer.at(30, function() return true end) 460 | end) 461 | end) 462 | goog_master:init() 463 | } 464 | --- config 465 | location /t { 466 | echo "OK"; 467 | } 468 | --- request 469 | GET /t 470 | --- wait: 3 471 | --- error_log 472 | resolver master failed to create resolve timer for domain 'google.com' 473 | 474 | 475 | === TEST 17: obey min / max ttl 476 | --- main_config 477 | env DNS_SERVER_IP=8.8.8.8; 478 | --- http_config 479 | lua_shared_dict test_res 1m; 480 | init_by_lua_block { 481 | local resolver_master = require "resolver.master" 482 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 483 | goog_master:set({ 484 | { 485 | address = "17.0.0.1", 486 | ttl = 300 487 | }, 488 | { 489 | address = "17.0.0.2", 490 | ttl = 20 491 | }, 492 | { 493 | address = "17.0.0.3", 494 | ttl = 1 495 | } 496 | }, 1482624000) 497 | } 498 | --- config 499 | location /t { 500 | content_by_lua_block { 501 | local keys = ngx.shared.test_res:get_keys() 502 | for i, k in ipairs(keys) do 503 | local v, err = ngx.shared.test_res:get(k) 504 | ngx.say(k .. "=" .. v) 505 | end 506 | } 507 | } 508 | --- request 509 | GET /t 510 | --- response_body 511 | google.com_17.0.0.1=1482624030 512 | google.com_17.0.0.2=1482624020 513 | google.com_17.0.0.3=1482624010 514 | --- no_error_log 515 | [error] 516 | 517 | 518 | === TEST 18: next resolve time 519 | --- main_config 520 | env DNS_SERVER_IP=8.8.8.8; 521 | --- http_config 522 | lua_shared_dict test_res 1m; 523 | init_by_lua_block { 524 | local resolver_master = require "resolver.master" 525 | goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, 10, 30, 2) 526 | } 527 | --- config 528 | location /t { 529 | content_by_lua_block { 530 | ngx.say(goog_master:set({{ 531 | address = "17.0.0.1", 532 | ttl = 300 533 | }})) 534 | ngx.say(goog_master:set({{ 535 | address = "17.0.0.1", 536 | ttl = 0 537 | }})) 538 | ngx.say(goog_master:set({ 539 | { 540 | address = "17.0.0.1", 541 | ttl = 15 542 | }, 543 | { 544 | address = "17.0.0.2", 545 | ttl = 25 546 | }, 547 | { 548 | address = "17.0.0.3", 549 | ttl = 20 550 | } 551 | }, 1482624000)) 552 | } 553 | } 554 | --- request 555 | GET /t 556 | --- response_body 557 | 28 558 | 8 559 | 13 560 | --- no_error_log 561 | [error] 562 | 563 | 564 | === TEST 19: filter banned 127.0.53.53 and 127.0.0.1 ip's 565 | --- main_config 566 | env DNS_SERVER_IP=8.8.8.8; 567 | --- http_config 568 | lua_shared_dict test_res 1m; 569 | init_by_lua_block { 570 | local resolver_master = require "resolver.master" 571 | local goog_master = resolver_master:new("test_res", "google.com", {os.getenv("DNS_SERVER_IP")}, nil, nil, nil, {"127.0.0.1", "127.0.53.53"}) 572 | goog_master:set({ 573 | { 574 | address = "17.0.0.1", 575 | ttl = 300 576 | }, 577 | { 578 | address = "127.0.0.1", -- banned 579 | ttl = 300 580 | }, 581 | { 582 | address = "127.0.53.53", -- banned 583 | ttl = 300 584 | }, 585 | }, 1482624000) 586 | } 587 | --- config 588 | location /t { 589 | content_by_lua_block { 590 | local keys = ngx.shared.test_res:get_keys() 591 | for i, k in ipairs(keys) do 592 | local v, err = ngx.shared.test_res:get(k) 593 | if v ~= "_master_" then 594 | ngx.say(k) 595 | ngx.say(v) 596 | end 597 | end 598 | } 599 | } 600 | --- request 601 | GET /t 602 | --- response_body 603 | google.com_17.0.0.1 604 | 1482624300 605 | --- no_error_log 606 | [error] 607 | 608 | 609 | === TEST 20: query hosts for sub-domain utilizing CNAME 610 | --- main_config 611 | env DNS_SERVER_IP=8.8.8.8; 612 | --- http_config 613 | lua_shared_dict test_res 1m; 614 | init_by_lua_block { 615 | local resolver_master = require "resolver.master" 616 | goog_master = resolver_master:new("test_res", "www.microsoft.com", {os.getenv("DNS_SERVER_IP")}, 10, 30) 617 | } 618 | init_worker_by_lua_block { 619 | goog_master:init() 620 | } 621 | --- config 622 | location /t { 623 | content_by_lua_block { 624 | ngx.sleep(2) 625 | local keys = ngx.shared.test_res:get_keys() 626 | for i, k in ipairs(keys) do 627 | local v, err = ngx.shared.test_res:get(k) 628 | if v ~= "_master_" then 629 | ngx.say(k) 630 | ngx.say(v) 631 | break 632 | end 633 | end 634 | } 635 | } 636 | --- request 637 | GET /t 638 | --- response_body_like chop 639 | ^www.microsoft.com_\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*\d+$ 640 | --- no_error_log 641 | [error] 642 | -------------------------------------------------------------------------------- /t/version.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket 'no_plan'; 4 | 5 | run_tests(); 6 | 7 | __DATA__ 8 | 9 | === TEST 1: master version 10 | --- http_config 11 | lua_shared_dict ver 12k; 12 | init_by_lua_block { 13 | local resolver_master = require "resolver.master" 14 | ngx.shared.ver:set("version", resolver_master._VERSION) 15 | } 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local ver, err = ngx.shared.ver:get("version") 20 | ngx.say("version" .. ver) 21 | } 22 | } 23 | --- request 24 | GET /t 25 | --- response_body_like chop 26 | ^version\d+\.\d+$ 27 | 28 | 29 | 30 | === TEST 2: worker version 31 | --- http_config 32 | upstream backend { 33 | server 0.0.0.1; # just an invalid address as a place holder 34 | 35 | balancer_by_lua_block { 36 | local balancer = require "ngx.balancer" 37 | balancer.set_current_peer("127.0.0.1", 8080) 38 | local resolver_client = require "resolver.client" 39 | ngx.log(ngx.ERR, "version", resolver_client._VERSION) 40 | } 41 | } 42 | server { 43 | listen 8080; 44 | location = /fake { 45 | echo "OK"; 46 | } 47 | } 48 | --- config 49 | location /t { 50 | proxy_pass http://backend/fake; 51 | } 52 | --- request 53 | GET /t 54 | --- error_log eval 55 | qr/version\d+\.\d+/ --------------------------------------------------------------------------------