kong/spec/01-unit/21-dns-client/02-client_spec.lua (1,462 lines of code) (raw):
local writefile = require("pl.utils").writefile
local tempfilename = require("pl.path").tmpname
local pretty = require("pl.pretty").write
-- empty records and not found errors should be identical, hence we
-- define a constant for that error message
local NOT_FOUND_ERROR = "dns server error: 3 name error"
local EMPTY_ERROR = "dns client error: 101 empty record received"
local BAD_IPV4_ERROR = "dns client error: 102 invalid name, bad IPv4"
local BAD_IPV6_ERROR = "dns client error: 103 invalid name, bad IPv6"
local gettime, sleep
if ngx then
gettime = ngx.now
sleep = ngx.sleep
else
local socket = require("socket")
gettime = socket.gettime
sleep = socket.sleep
end
-- simple debug function
-- luacheck: push no unused
local dump = function(...)
print(pretty({...}))
end
-- luacheck: pop
describe("[DNS client]", function()
local client, resolver, query_func
before_each(function()
client = require("kong.resty.dns.client")
resolver = require("resty.dns.resolver")
-- you can replace this `query_func` upvalue to spy on resolver query calls.
-- This default will just call the original resolver (hence is transparent)
query_func = function(self, original_query_func, name, options)
return original_query_func(self, name, options)
end
-- patch the resolver lib, such that any new resolver created will query
-- using the `query_func` upvalue defined above
local old_new = resolver.new
resolver.new = function(...)
local r, err = old_new(...)
if not r then
return nil, err
end
local original_query_func = r.query
r.query = function(self, ...)
return query_func(self, original_query_func, ...)
end
return r
end
end)
after_each(function()
package.loaded["kong.resty.dns.client"] = nil
package.loaded["resty.dns.resolver"] = nil
client = nil
resolver = nil
query_func = nil
end)
describe("initialization", function()
it("does not fail with no nameservers", function()
-- empty list fallsback on resolv.conf
assert.has.no.error(function() client.init( {nameservers = {} } ) end)
assert.has.no.error(function() client.init( {nameservers = {}, resolvConf = {} } ) end)
end)
it("skips ipv6 nameservers with scopes", function()
assert.has.no.error(function() client.init({
enable_ipv6 = true,
resolvConf = {"nameserver [fe80::1%enp0s20f0u1u1]"},
})
end)
local ip, port = client.toip("thijsschreijer.nl")
assert.is_nil(ip)
assert.not_matches([[failed to parse host name "[fe80::1%enp0s20f0u1u1]": invalid IPv6 address]], port, nil, true)
assert.matches([[failed to create a resolver: no nameservers specified]], port, nil, true)
end)
it("fails with order being empty", function()
-- fails with an empty one
assert.has.error(
function() client.init({order = {}}) end,
"Invalid order list; cannot be empty"
)
end)
it("fails with order containing an unknown type", function()
-- fails with an unknown one
assert.has.error(
function() client.init({order = {"LAST", "a", "aa"}}) end,
"Invalid dns record type in order array; aa"
)
end)
it("succeeds with order unset", function()
assert.is.True(client.init({order = nil}))
end)
it("succeeds without i/o access", function()
local result, err = assert(client.init({
nameservers = { "8.8.8.8:53" },
hosts = {}, -- empty tables to parse to prevent defaulting to /etc/hosts
resolvConf = {}, -- and resolv.conf files
}))
assert.is.True(result)
assert.is.Nil(err)
assert.are.equal(#client.getcache(), 0) -- no hosts file record should have been imported
end)
describe("inject localhost:", function()
it("if absent", function()
local result, err, record
result, err = assert(client.init({
nameservers = { "8.8.8.8:53" },
resolvConf = {},
hosts = {},
}))
assert.is.True(result)
assert.is.Nil(err)
record = client.getcache():get("28:localhost")
assert.equal("[::1]", record[1].address)
record = client.getcache():get("1:localhost")
assert.equal("127.0.0.1", record[1].address)
end)
it("not if ipv4 exists", function()
local result, err, record
result, err = assert(client.init({
nameservers = { "8.8.8.8:53" },
resolvConf = {},
hosts = {"1.2.3.4 localhost"},
}))
assert.is.True(result)
assert.is.Nil(err)
-- IPv6 is not defined
record = client.getcache():get("28:localhost")
assert.is_nil(record)
-- IPv4 is not overwritten
record = client.getcache():get("1:localhost")
assert.equal("1.2.3.4", record[1].address)
end)
it("not if ipv6 exists", function()
local result, err, record
result, err = assert(client.init({
nameservers = { "8.8.8.8:53" },
resolvConf = {},
hosts = {"::1:2:3:4 localhost"},
}))
assert.is.True(result)
assert.is.Nil(err)
-- IPv6 is not overwritten
record = client.getcache():get("28:localhost")
assert.equal("[::1:2:3:4]", record[1].address)
-- IPv4 is not defined
record = client.getcache():get("1:localhost")
assert.is_nil(record)
end)
end)
end)
describe("iterating searches", function()
describe("without type", function()
it("works with a 'search' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.one.com:33',
'host.two.com:33',
'host:33',
'host.one.com:1',
'host.two.com:1',
'host:1',
'host.one.com:28',
'host.two.com:28',
'host:28',
'host.one.com:5',
'host.two.com:5',
'host:5',
}, list)
end)
it("works with a 'search .' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search .",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host:33',
'host:1',
'host:28',
'host:5',
}, list)
end)
it("works with a 'domain' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"domain local.domain.com",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.local.domain.com:33',
'host:33',
'host.local.domain.com:1',
'host:1',
'host.local.domain.com:28',
'host:28',
'host.local.domain.com:5',
'host:5',
}, list)
end)
it("handles last successful type", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local lrucache = client.getcache()
-- insert a last successful type
local hostname = "host"
lrucache:set(hostname, client.TYPE_CNAME)
local list = {}
for qname, qtype in client._search_iter(hostname, nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.one.com:5',
'host.two.com:5',
'host:5',
'host.one.com:33',
'host.two.com:33',
'host:33',
'host.one.com:1',
'host.two.com:1',
'host:1',
'host.one.com:28',
'host.two.com:28',
'host:28',
}, list)
end)
end)
describe("FQDN without type", function()
it("works with a 'search' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host.", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:33',
'host.:1',
'host.:28',
'host.:5',
}, list)
end)
it("works with a 'search .' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search .",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host.", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:33',
'host.:1',
'host.:28',
'host.:5',
}, list)
end)
it("works with a 'domain' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"domain local.domain.com",
"options ndots:1",
}
}))
local list = {}
for qname, qtype in client._search_iter("host.", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:33',
'host.:1',
'host.:28',
'host.:5',
}, list)
end)
it("handles last successful type", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local lrucache = client.getcache()
-- insert a last successful type
local hostname = "host."
lrucache:set(hostname, client.TYPE_CNAME)
local list = {}
for qname, qtype in client._search_iter(hostname, nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:5',
'host.:33',
'host.:1',
'host.:28',
}, list)
end)
end)
describe("with type", function()
it("works with a 'search' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.one.com:28',
'host.two.com:28',
'host:28',
}, list)
end)
it("works with a 'domain' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"domain local.domain.com",
"options ndots:1",
}
}))
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.local.domain.com:28',
'host:28',
}, list)
end)
it("ignores last successful type", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
-- insert a last successful type
client.getcache()["host"] = client.TYPE_CNAME
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.one.com:28',
'host.two.com:28',
'host:28',
}, list)
end)
end)
describe("FQDN with type", function()
it("works with a 'search' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host.", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:28',
}, list)
end)
it("works with a 'domain' option", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"domain local.domain.com",
"options ndots:1",
}
}))
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host.", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:28',
}, list)
end)
it("ignores last successful type", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
-- insert a last successful type
client.getcache()["host"] = client.TYPE_CNAME
local list = {}
-- search using IPv6 type
for qname, qtype in client._search_iter("host.", client.TYPE_AAAA) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host.:28',
}, list)
end)
end)
it("honours 'ndots'", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
}
}))
local list = {}
-- now use a name with a dot in it
for qname, qtype in client._search_iter("local.host", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'local.host:33',
'local.host.one.com:33',
'local.host.two.com:33',
'local.host:1',
'local.host.one.com:1',
'local.host.two.com:1',
'local.host:28',
'local.host.one.com:28',
'local.host.two.com:28',
'local.host:5',
'local.host.one.com:5',
'local.host.two.com:5',
}, list)
end)
it("hosts file always resolves first, overriding `ndots`", function()
assert(client.init({
resolvConf = {
"nameserver 8.8.8.8",
"search one.com two.com",
"options ndots:1",
},
hosts = {
"127.0.0.1 host",
"::1 host",
},
order = { "LAST", "SRV", "A", "AAAA", "CNAME" }
}))
local list = {}
for qname, qtype in client._search_iter("host", nil) do
table.insert(list, tostring(qname)..":"..tostring(qtype))
end
assert.same({
'host:1',
'host.one.com:1',
'host.two.com:1',
'host.one.com:33',
'host.two.com:33',
'host:33',
'host:28',
'host.one.com:28',
'host.two.com:28',
'host.one.com:5',
'host.two.com:5',
'host:5',
}, list)
end)
end)
it("fetching a record without nameservers errors", function()
assert(client.init({ resolvConf = {} }))
local host = "thijsschreijer.nl"
local typ = client.TYPE_A
local answers, err, _ = client.resolve(host, { qtype = typ })
assert.is_nil(answers)
assert(err:find("failed to create a resolver: no nameservers specified"))
end)
it("fetching a TXT record", function()
assert(client.init())
local host = "txttest.thijsschreijer.nl"
local typ = client.TYPE_TXT
local answers, err, try_list = client.resolve(host, { qtype = typ })
assert(answers, (err or "") .. tostring(try_list))
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(#answers, 1)
end)
it("fetching a CNAME record", function()
assert(client.init())
local host = "smtp.thijsschreijer.nl"
local typ = client.TYPE_CNAME
local answers = assert(client.resolve(host, { qtype = typ }))
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(#answers, 1)
end)
it("fetching a CNAME record FQDN", function()
assert(client.init())
local host = "smtp.thijsschreijer.nl"
local typ = client.TYPE_CNAME
local answers = assert(client.resolve(host .. ".", { qtype = typ }))
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(#answers, 1)
end)
it("expire and touch times", function()
assert(client.init())
local host = "txttest.thijsschreijer.nl"
local typ = client.TYPE_TXT
local answers, _, _ = assert(client.resolve(host, { qtype = typ }))
local now = gettime()
local touch_diff = math.abs(now - answers.touch)
local ttl_diff = math.abs((now + answers[1].ttl) - answers.expire)
assert(touch_diff < 0.01, "Expected difference to be near 0; "..
tostring(touch_diff))
assert(ttl_diff < 0.01, "Expected difference to be near 0; "..
tostring(ttl_diff))
sleep(1)
-- fetch again, now from cache
local oldtouch = answers.touch
local answers2 = assert(client.resolve(host, { qtype = typ }))
assert.are.equal(answers, answers2) -- cached table, so must be same
assert.are.not_equal(oldtouch, answers.touch)
now = gettime()
touch_diff = math.abs(now - answers.touch)
ttl_diff = math.abs((now + answers[1].ttl) - answers.expire)
assert(touch_diff < 0.01, "Expected difference to be near 0; "..
tostring(touch_diff))
assert((0.990 < ttl_diff) and (ttl_diff < 1.01),
"Expected difference to be near 1; "..tostring(ttl_diff))
end)
it("fetching names case insensitive", function()
assert(client.init())
query_func = function(self, original_query_func, name, options)
return {
{
name = "some.UPPER.case",
type = client.TYPE_A,
ttl = 30,
}
}
end
local res, _, _ = client.resolve(
"some.upper.CASE",
{ qtype = client.TYPE_A },
false)
assert.equal(1, #res)
assert.equal("some.upper.case", res[1].name)
end)
it("fetching multiple A records", function()
assert(client.init())
local host = "atest.thijsschreijer.nl"
local typ = client.TYPE_A
local answers = assert(client.resolve(host, { qtype = typ }))
assert.are.equal(#answers, 2)
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(host, answers[2].name)
assert.are.equal(typ, answers[2].type)
end)
it("fetching multiple A records FQDN", function()
assert(client.init())
local host = "atest.thijsschreijer.nl"
local typ = client.TYPE_A
local answers = assert(client.resolve(host .. ".", { qtype = typ }))
assert.are.equal(#answers, 2)
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(host, answers[2].name)
assert.are.equal(typ, answers[2].type)
end)
it("fetching A record redirected through 2 CNAME records (un-typed)", function()
assert(client.init({ search = {}, }))
local lrucache = client.getcache()
--[[
This test might fail. Recurse flag is on by default. This means that the first return
includes the cname records, but the second one (within the ttl) will only include the
A-record.
Note that this is not up to the client code, but it's done out of our control by the
dns server.
If we turn on the 'no_recurse = true' option, then the dns server might refuse the request
(error nr 5).
So effectively the first time the test runs, it's ok. Immediately running it again will
make it fail. Wait for the ttl to expire, then it will work again.
This does not affect client side code, as the result is always the final A record.
--]]
local host = "smtp.thijsschreijer.nl"
local typ = client.TYPE_A
local answers, _, _ = assert(client.resolve(host))
-- check first CNAME
local key1 = client.TYPE_CNAME..":"..host
local entry1 = lrucache:get(key1)
assert.are.equal(host, entry1[1].name) -- the 1st record is the original 'smtp.thijsschreijer.nl'
assert.are.equal(client.TYPE_CNAME, entry1[1].type) -- and that is a CNAME
-- check second CNAME
local key2 = client.TYPE_CNAME..":"..entry1[1].cname
local entry2 = lrucache:get(key2)
assert.are.equal(entry1[1].cname, entry2[1].name) -- the 2nd is the middle 'thuis.thijsschreijer.nl'
assert.are.equal(client.TYPE_CNAME, entry2[1].type) -- and that is also a CNAME
-- check second target to match final record
assert.are.equal(entry2[1].cname, answers[1].name)
assert.are.not_equal(host, answers[1].name) -- we got final name 'wdnaste.duckdns.org'
assert.are.equal(typ, answers[1].type) -- we got a final A type record
assert.are.equal(#answers, 1)
-- check last successful lookup references
local lastsuccess3 = lrucache:get(answers[1].name)
local lastsuccess2 = lrucache:get(entry2[1].name)
local lastsuccess1 = lrucache:get(entry1[1].name)
assert.are.equal(client.TYPE_A, lastsuccess3)
assert.are.equal(client.TYPE_CNAME, lastsuccess2)
assert.are.equal(client.TYPE_CNAME, lastsuccess1)
end)
it("fetching multiple SRV records (un-typed)", function()
assert(client.init())
local host = "srvtest.thijsschreijer.nl"
local typ = client.TYPE_SRV
-- un-typed lookup
local answers = assert(client.resolve(host))
assert.are.equal(host, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(host, answers[2].name)
assert.are.equal(typ, answers[2].type)
assert.are.equal(host, answers[3].name)
assert.are.equal(typ, answers[3].type)
assert.are.equal(#answers, 3)
end)
it("fetching multiple SRV records through CNAME (un-typed)", function()
assert(client.init({ search = {}, }))
local lrucache = client.getcache()
local host = "cname2srv.thijsschreijer.nl"
local typ = client.TYPE_SRV
-- un-typed lookup
local answers = assert(client.resolve(host))
-- first check CNAME
local key = client.TYPE_CNAME..":"..host
local entry = lrucache:get(key)
assert.are.equal(host, entry[1].name)
assert.are.equal(client.TYPE_CNAME, entry[1].type)
-- check final target
assert.are.equal(entry[1].cname, answers[1].name)
assert.are.equal(typ, answers[1].type)
assert.are.equal(entry[1].cname, answers[2].name)
assert.are.equal(typ, answers[2].type)
assert.are.equal(entry[1].cname, answers[3].name)
assert.are.equal(typ, answers[3].type)
assert.are.equal(#answers, 3)
end)
it("fetching non-type-matching records", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local host = "srvtest.thijsschreijer.nl"
local typ = client.TYPE_A --> the entry is SRV not A
local answers, err, _ = client.resolve(host, {qtype = typ})
assert.is_nil(answers) -- returns nil
assert.equal(EMPTY_ERROR, err)
end)
it("fetching non-existing records", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local host = "IsNotHere.thijsschreijer.nl"
local answers, err, _ = client.resolve(host)
assert.is_nil(answers)
assert.equal(NOT_FOUND_ERROR, err)
end)
it("fetching IPv4 address as A type", function()
assert(client.init())
local lrucache = client.getcache()
local host = "1.2.3.4"
local answers = assert(client.resolve(host, { qtype = client.TYPE_A }))
assert.are.equal(#answers, 1)
assert.are.equal(client.TYPE_A, answers[1].type)
assert.are.equal(10*365*24*60*60, answers[1].ttl) -- 10 year ttl
assert.equal(client.TYPE_A, lrucache:get(host))
end)
it("fetching IPv4 address as SRV type", function()
assert(client.init())
local callcount = 0
query_func = function(self, original_query_func, name, options)
callcount = callcount + 1
return original_query_func(self, name, options)
end
local _, err, _ = client.resolve(
"1.2.3.4",
{ qtype = client.TYPE_SRV },
false
)
assert.equal(0, callcount)
assert.equal(BAD_IPV4_ERROR, err)
end)
it("fetching IPv6 address as AAAA type", function()
assert(client.init())
local host = "[1:2::3:4]"
local answers = assert(client.resolve(host, { qtype = client.TYPE_AAAA }))
assert.are.equal(#answers, 1)
assert.are.equal(client.TYPE_AAAA, answers[1].type)
assert.are.equal(10*365*24*60*60, answers[1].ttl) -- 10 year ttl
assert.are.equal(host, answers[1].address)
local lrucache = client.getcache()
assert.equal(client.TYPE_AAAA, lrucache:get(host))
end)
it("fetching IPv6 address as AAAA type (without brackets)", function()
assert(client.init())
local host = "1:2::3:4"
local answers = assert(client.resolve(host, { qtype = client.TYPE_AAAA }))
assert.are.equal(#answers, 1)
assert.are.equal(client.TYPE_AAAA, answers[1].type)
assert.are.equal(10*365*24*60*60, answers[1].ttl) -- 10 year ttl
assert.are.equal("["..host.."]", answers[1].address) -- brackets added
local lrucache = client.getcache()
assert.equal(client.TYPE_AAAA, lrucache:get(host))
end)
it("fetching IPv6 address as SRV type", function()
assert(client.init())
local callcount = 0
query_func = function(self, original_query_func, name, options)
callcount = callcount + 1
return original_query_func(self, name, options)
end
local _, err, _ = client.resolve(
"[1:2::3:4]",
{ qtype = client.TYPE_SRV },
false
)
assert.equal(0, callcount)
assert.equal(BAD_IPV6_ERROR, err)
end)
it("fetching invalid IPv6 address", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local host = "[1::2:3::4]" -- 2x double colons
local answers, err, history = client.resolve(host)
assert.is_nil(answers)
assert.equal(BAD_IPV6_ERROR, err)
assert(tostring(history):find("bad IPv6", nil, true))
end)
it("fetching IPv6 in an SRV record adds brackets",function()
assert(client.init())
local host = "hello.world"
local address = "::1"
local entry = {
{
type = client.TYPE_SRV,
target = address,
port = 321,
weight = 10,
priority = 10,
class = 1,
name = host,
ttl = 10,
},
}
query_func = function(self, original_query_func, name, options)
if name == host and options.qtype == client.TYPE_SRV then
return entry
end
return original_query_func(self, name, options)
end
local res, _, _ = client.resolve(
host,
{ qtype = client.TYPE_SRV },
false
)
assert.equal("["..address.."]", res[1].target)
end)
it("recursive lookups failure - single resolve", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
query_func = function(self, original_query_func, name, opts)
if name ~= "hello.world" and (opts or {}).qtype ~= client.TYPE_CNAME then
return original_query_func(self, name, opts)
end
return {
{
type = client.TYPE_CNAME,
cname = "hello.world",
class = 1,
name = "hello.world",
ttl = 30,
},
}
end
local result, err, _ = client.resolve("hello.world")
assert.is_nil(result)
assert.are.equal("recursion detected", err)
end)
it("recursive lookups failure - single", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local lrucache = client.getcache()
local entry1 = {
{
type = client.TYPE_CNAME,
cname = "hello.world",
class = 1,
name = "hello.world",
ttl = 0,
},
touch = 0,
expire = 0,
}
-- insert in the cache
lrucache:set(entry1[1].type..":"..entry1[1].name, entry1)
-- Note: the bad case would be that the below lookup would hang due to round-robin on an empty table
local result, err, _ = client.resolve("hello.world", nil, true)
assert.is_nil(result)
assert.are.equal("recursion detected", err)
end)
it("recursive lookups failure - multi", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local lrucache = client.getcache()
local entry1 = {
{
type = client.TYPE_CNAME,
cname = "bye.bye.world",
class = 1,
name = "hello.world",
ttl = 0,
},
touch = 0,
expire = 0,
}
local entry2 = {
{
type = client.TYPE_CNAME,
cname = "hello.world",
class = 1,
name = "bye.bye.world",
ttl = 0,
},
touch = 0,
expire = 0,
}
-- insert in the cache
lrucache:set(entry1[1].type..":"..entry1[1].name, entry1)
lrucache:set(entry2[1].type..":"..entry2[1].name, entry2)
-- Note: the bad case would be that the below lookup would hang due to round-robin on an empty table
local result, err, _ = client.resolve("hello.world", nil, true)
assert.is_nil(result)
assert.are.equal("recursion detected", err)
end)
it("resolving from the /etc/hosts file; preferred A or AAAA order", function()
local f = tempfilename()
writefile(f, [[
127.3.2.1 localhost
1::2 localhost
]])
assert(client.init(
{
hosts = f,
order = {"SRV", "CNAME", "A", "AAAA"},
}))
local lrucache = client.getcache()
assert.equal(client.TYPE_A, lrucache:get("localhost")) -- success set to A as it is the preferred option
assert(client.init(
{
hosts = f,
order = {"SRV", "CNAME", "AAAA", "A"},
}))
lrucache = client.getcache()
assert.equal(client.TYPE_AAAA, lrucache:get("localhost")) -- success set to AAAA as it is the preferred option
end)
it("resolving from the /etc/hosts file", function()
local f = tempfilename()
writefile(f, [[
127.3.2.1 localhost
1::2 localhost
123.123.123.123 mashape
1234::1234 kong.for.president
]])
assert(client.init({ hosts = f }))
os.remove(f)
local answers, err = client.resolve("localhost", {qtype = client.TYPE_A})
assert.is.Nil(err)
assert.are.equal(answers[1].address, "127.3.2.1")
answers, err = client.resolve("localhost", {qtype = client.TYPE_AAAA})
assert.is.Nil(err)
assert.are.equal(answers[1].address, "[1::2]")
answers, err = client.resolve("mashape", {qtype = client.TYPE_A})
assert.is.Nil(err)
assert.are.equal(answers[1].address, "123.123.123.123")
answers, err = client.resolve("kong.for.president", {qtype = client.TYPE_AAAA})
assert.is.Nil(err)
assert.are.equal(answers[1].address, "[1234::1234]")
end)
describe("toip() function", function()
it("A/AAAA-record, round-robin",function()
assert(client.init({ search = {}, }))
local host = "atest.thijsschreijer.nl"
local answers = assert(client.resolve(host))
answers.last_index = nil -- make sure to clean
local ips = {}
for _,rec in ipairs(answers) do ips[rec.address] = true end
local order = {}
for n = 1, #answers do
local ip = client.toip(host)
ips[ip] = nil
order[n] = ip
end
-- this table should be empty again
assert.is_nil(next(ips))
-- do again, and check same order
for n = 1, #order do
local ip = client.toip(host)
assert.same(order[n], ip)
end
end)
it("SRV-record, round-robin on lowest prio",function()
assert(client.init())
local lrucache = client.getcache()
local host = "hello.world.test"
local entry = {
{
type = client.TYPE_SRV,
target = "1.2.3.4",
port = 8000,
weight = 5,
priority = 10,
class = 1,
name = host,
ttl = 10,
},
{
type = client.TYPE_SRV,
target = "1.2.3.4",
port = 8001,
weight = 5,
priority = 20,
class = 1,
name = host,
ttl = 10,
},
{
type = client.TYPE_SRV,
target = "1.2.3.4",
port = 8002,
weight = 5,
priority = 10,
class = 1,
name = host,
ttl = 10,
},
touch = 0,
expire = gettime()+10,
}
-- insert in the cache
lrucache:set(entry[1].type..":"..entry[1].name, entry)
local results = {}
for _ = 1,20 do
local _, port = client.toip(host)
results[port] = (results[port] or 0) + 1
end
-- 20 passes, each should get 10
assert.equal(0, results[8001] or 0) --priority 20, no hits
assert.equal(10, results[8000] or 0) --priority 10, 50% of hits
assert.equal(10, results[8002] or 0) --priority 10, 50% of hits
end)
it("SRV-record with 1 entry, round-robin",function()
assert(client.init())
local lrucache = client.getcache()
local host = "hello.world"
local entry = {
{
type = client.TYPE_SRV,
target = "1.2.3.4",
port = 321,
weight = 10,
priority = 10,
class = 1,
name = host,
ttl = 10,
},
touch = 0,
expire = gettime()+10,
}
-- insert in the cache
lrucache:set(entry[1].type..":"..entry[1].name, entry)
-- repeated lookups, as the first will simply serve the first entry
-- and the only second will setup the round-robin scheme, this is
-- specific for the SRV record type, due to the weights
for _ = 1 , 10 do
local ip, port = assert(client.toip(host))
assert.equal("1.2.3.4", ip)
assert.equal(321, port)
end
end)
it("SRV-record with 0-weight, round-robin",function()
assert(client.init())
local lrucache = client.getcache()
local host = "hello.world"
local entry = {
{
type = client.TYPE_SRV,
target = "1.2.3.4",
port = 321,
weight = 0, --> weight 0
priority = 10,
class = 1,
name = host,
ttl = 10,
},
{
type = client.TYPE_SRV,
target = "1.2.3.5",
port = 321,
weight = 50, --> weight 50
priority = 10,
class = 1,
name = host,
ttl = 10,
},
{
type = client.TYPE_SRV,
target = "1.2.3.6",
port = 321,
weight = 50, --> weight 50
priority = 10,
class = 1,
name = host,
ttl = 10,
},
touch = 0,
expire = gettime()+10,
}
-- insert in the cache
lrucache:set(entry[1].type..":"..entry[1].name, entry)
-- weight 0 will be weight 1, without any reduction in weight
-- of the other ones.
local track = {}
for _ = 1 , 202 do --> run around twice
local ip, _ = assert(client.toip(host))
track[ip] = (track[ip] or 0) + 1
end
assert.equal(100, track["1.2.3.5"])
assert.equal(100, track["1.2.3.6"])
assert.equal(2, track["1.2.3.4"])
end)
it("port passing",function()
assert(client.init())
local lrucache = client.getcache()
local entry_a = {
{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "a.record.test",
ttl = 10,
},
touch = 0,
expire = gettime()+10,
}
local entry_srv = {
{
type = client.TYPE_SRV,
target = "a.record.test",
port = 8001,
weight = 5,
priority = 20,
class = 1,
name = "srv.record.test",
ttl = 10,
},
touch = 0,
expire = gettime()+10,
}
-- insert in the cache
lrucache:set(entry_a[1].type..":"..entry_a[1].name, entry_a)
lrucache:set(entry_srv[1].type..":"..entry_srv[1].name, entry_srv)
local ip, port
local host = "a.record.test"
ip,port = client.toip(host)
assert.is_string(ip)
assert.is_nil(port)
ip, port = client.toip(host, 1234)
assert.is_string(ip)
assert.equal(1234, port)
host = "srv.record.test"
ip, port = client.toip(host)
assert.is_string(ip)
assert.is_number(port)
ip, port = client.toip(host, 0)
assert.is_string(ip)
assert.is_number(port)
assert.is_not.equal(0, port)
end)
it("port passing if SRV port=0",function()
assert(client.init({ search = {}, }))
local ip, port, host
host = "srvport0.thijsschreijer.nl"
ip, port = client.toip(host, 10)
assert.is_string(ip)
assert.is_number(port)
assert.is_equal(10, port)
ip, port = client.toip(host)
assert.is_string(ip)
assert.is_nil(port)
end)
it("recursive SRV pointing to itself",function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local ip, record, port, host, err, _
host = "srvrecurse.thijsschreijer.nl"
-- resolve SRV specific should return the record including its
-- recursive entry
record, err, _ = client.resolve(host, { qtype = client.TYPE_SRV })
assert.is_table(record)
assert.equal(1, #record)
assert.equal(host, record[1].target)
assert.equal(host, record[1].name)
assert.is_nil(err)
-- default order, SRV, A; the recursive SRV record fails, and it falls
-- back to the IP4 address
ip, port, _ = client.toip(host)
assert.is_string(ip)
assert.is_equal("10.0.0.44", ip)
assert.is_nil(port)
end)
it("resolving in correct record-type order",function()
local function config()
-- function to insert 2 records in the cache
local A_entry = {
{
type = client.TYPE_A,
address = "5.6.7.8",
class = 1,
name = "hello.world",
ttl = 10,
},
touch = 0,
expire = gettime()+10, -- active
}
local AAAA_entry = {
{
type = client.TYPE_AAAA,
address = "::1",
class = 1,
name = "hello.world",
ttl = 10,
},
touch = 0,
expire = gettime()+10, -- active
}
-- insert in the cache
local lrucache = client.getcache()
lrucache:set(A_entry[1].type..":"..A_entry[1].name, A_entry)
lrucache:set(AAAA_entry[1].type..":"..AAAA_entry[1].name, AAAA_entry)
end
assert(client.init({order = {"AAAA", "A"}}))
config()
local ip = client.toip("hello.world")
assert.equals(ip, "::1")
assert(client.init({order = {"A", "AAAA"}}))
config()
ip = client.toip("hello.world")
assert.equals(ip, "5.6.7.8")
end)
it("handling of empty responses", function()
assert(client.init())
local empty_entry = {
touch = 0,
expire = 0,
}
-- insert in the cache
client.getcache()[client.TYPE_A..":".."hello.world"] = empty_entry
-- Note: the bad case would be that the below lookup would hang due to round-robin on an empty table
local ip, port = client.toip("hello.world", 123, true)
assert.is_nil(ip)
assert.is.string(port) -- error message
end)
it("recursive lookups failure", function()
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
local lrucache = client.getcache()
local entry1 = {
{
type = client.TYPE_CNAME,
cname = "bye.bye.world",
class = 1,
name = "hello.world",
ttl = 10,
},
touch = 0,
expire = gettime()+10, -- active
}
local entry2 = {
{
type = client.TYPE_CNAME,
cname = "hello.world",
class = 1,
name = "bye.bye.world",
ttl = 10,
},
touch = 0,
expire = gettime()+10, -- active
}
-- insert in the cache
lrucache:set(entry1[1].type..":"..entry1[1].name, entry1)
lrucache:set(entry2[1].type..":"..entry2[1].name, entry2)
-- Note: the bad case would be that the below lookup would hang due to round-robin on an empty table
local ip, port, _ = client.toip("hello.world", 123, true)
assert.is_nil(ip)
assert.are.equal("recursion detected", port)
end)
end)
it("verifies validTtl", function()
local validTtl = 0.1
local emptyTtl = 0.1
local staleTtl = 0.1
local qname = "konghq.com"
assert(client.init({
emptyTtl = emptyTtl,
staleTtl = staleTtl,
validTtl = validTtl,
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
-- mock query function to return a default record
query_func = function(self, original_query_func, name, options)
return {
{
type = client.TYPE_A,
address = "5.6.7.8",
class = 1,
name = qname,
ttl = 10, -- should be overridden by the validTtl setting
},
}
end
-- do a query
local res1, _, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.equal(validTtl, res1[1].ttl)
assert.is_near(validTtl, res1.expire - gettime(), 0.1)
end)
it("verifies ttl and caching of empty responses and name errors", function()
--empty/error responses should be cached for a configurable time
local emptyTtl = 0.1
local staleTtl = 0.1
local qname = "really.really.really.does.not.exist.thijsschreijer.nl"
assert(client.init({
emptyTtl = emptyTtl,
staleTtl = staleTtl,
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
-- mock query function to count calls
local call_count = 0
query_func = function(self, original_query_func, name, options)
call_count = call_count + 1
return original_query_func(self, name, options)
end
-- make a first request, populating the cache
local res1, res2, err1, err2, _
res1, err1, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res1)
assert.are.equal(1, call_count)
assert.are.equal(NOT_FOUND_ERROR, err1)
res1 = assert(client.getcache():get(client.TYPE_A..":"..qname))
-- make a second request, result from cache, still called only once
res2, err2, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(1, call_count)
assert.are.equal(NOT_FOUND_ERROR, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.equal(res1, res2)
assert.falsy(res2.expired)
-- wait for expiry of Ttl and retry, still called only once
sleep(emptyTtl+0.5 * staleTtl)
res2, err2 = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(1, call_count)
assert.are.equal(NOT_FOUND_ERROR, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.equal(res1, res2)
assert.is_true(res2.expired) -- by now, record is marked as expired
-- wait for expiry of staleTtl and retry, should be called twice now
sleep(0.75 * staleTtl)
res2, err2 = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(2, call_count)
assert.are.equal(NOT_FOUND_ERROR, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.not_equal(res1, res2)
assert.falsy(res2.expired) -- new record, not expired
end)
it("verifies ttl and caching of (other) dns errors", function()
--empty responses should be cached for a configurable time
local badTtl = 0.1
local staleTtl = 0.1
local qname = "realname.com"
assert(client.init({
badTtl = badTtl,
staleTtl = staleTtl,
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
-- mock query function to count calls, and return errors
local call_count = 0
query_func = function(self, original_query_func, name, options)
call_count = call_count + 1
return { errcode = 5, errstr = "refused" }
end
-- initial request to populate the cache
local res1, res2, err1, err2, _
res1, err1, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res1)
assert.are.equal(1, call_count)
assert.are.equal("dns server error: 5 refused", err1)
res1 = assert(client.getcache():get(client.TYPE_A..":"..qname))
-- try again, from cache, should still be called only once
res2, err2, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(call_count, 1)
assert.are.equal(err1, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.are.equal(res1, res2)
assert.falsy(res1.expired)
-- wait for expiry of ttl and retry, still 1 call, but now stale result
sleep(badTtl + 0.5 * staleTtl)
res2, err2, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(call_count, 1)
assert.are.equal(err1, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.are.equal(res1, res2)
assert.is_true(res2.expired)
-- wait for expiry of staleTtl and retry, 2 calls, new result
sleep(0.75 * staleTtl)
res2, err2, _ = client.resolve(
qname,
{ qtype = client.TYPE_A }
)
assert.is_nil(res2)
assert.are.equal(call_count, 2) -- 2 calls now
assert.are.equal(err1, err2)
res2 = assert(client.getcache():get(client.TYPE_A..":"..qname))
assert.are_not.equal(res1, res2) -- a new record
assert.falsy(res2.expired)
end)
describe("verifies the polling of dns queries, retries, and wait times", function()
it("simultaneous lookups are synchronized to 1 lookup", function()
assert(client.init())
local coros = {}
local results = {}
local call_count = 0
query_func = function(self, original_query_func, name, options)
call_count = call_count + 1
sleep(0.5) -- make sure we take enough time so the other threads
-- will be waiting behind this one
return original_query_func(self, name, options)
end
-- we're going to schedule a whole bunch of queries, all of this
-- function, which does the same lookup and stores the result
local x = function()
-- the function is ran when started. So we must immediately yield
-- so the scheduler loop can first schedule them all before actually
-- starting resolving
coroutine.yield(coroutine.running())
local result, _, _ = client.resolve(
"thijsschreijer.nl",
{ qtype = client.TYPE_A }
)
table.insert(results, result)
end
-- schedule a bunch of the same lookups
for _ = 1, 10 do
local co = ngx.thread.spawn(x)
table.insert(coros, co)
end
-- all scheduled and waiting to start due to the yielding done.
-- now start them all
for i = 1, #coros do
ngx.thread.wait(coros[i]) -- this wait will resume the scheduled ones
end
-- now count the unique responses we got
local counters = {}
for _, r in ipairs(results) do
r = tostring(r)
counters[r] = (counters[r] or 0) + 1
end
local count = 0
for _ in pairs(counters) do count = count + 1 end
-- we should have a single result table, as all threads are supposed to
-- return the exact same table.
assert.equal(1,count)
end)
it("timeout while waiting", function()
-- basically the local function _synchronized_query
assert(client.init({
timeout = 500,
retrans = 1,
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
}))
-- insert a stub thats waits and returns a fixed record
local name = "thijsschreijer.nl"
query_func = function()
local ip = "1.4.2.3"
local entry = {
{
type = client.TYPE_A,
address = ip,
class = 1,
name = name,
ttl = 10,
},
touch = 0,
expire = gettime() + 10,
}
sleep(0.5) -- wait before we return the results
return entry
end
local coros = {}
local results = {}
-- we're going to schedule a whole bunch of queries, all of this
-- function, which does the same lookup and stores the result
local x = function()
-- the function is ran when started. So we must immediately yield
-- so the scheduler loop can first schedule them all before actually
-- starting resolving
coroutine.yield(coroutine.running())
local result, err, _ = client.resolve(name, {qtype = client.TYPE_A})
table.insert(results, (result or err))
end
-- schedule a bunch of the same lookups
for _ = 1, 10 do
local co = ngx.thread.spawn(x)
table.insert(coros, co)
end
-- all scheduled and waiting to start due to the yielding done.
-- now start them all
for i = 1, #coros do
ngx.thread.wait(coros[i]) -- this wait will resume the scheduled ones
end
-- all results are equal, as they all will wait for the first response
for i = 1, 10 do
assert.equal("dns lookup pool exceeded retries (1): timeout", results[i])
end
end)
end)
it("noSynchronisation == true, queries on each request", function()
-- basically the local function _synchronized_query
assert(client.init({
resolvConf = {
-- resolv.conf without `search` and `domain` options
"nameserver 8.8.8.8",
},
noSynchronisation = true,
}))
-- insert a stub thats waits and returns a fixed record
local call_count = 0
local name = "thijsschreijer.nl"
query_func = function()
local ip = "1.4.2.3"
local entry = {
{
type = client.TYPE_A,
address = ip,
class = 1,
name = name,
ttl = 10,
},
touch = 0,
expire = gettime() + 10,
}
sleep(1) -- wait before we return the results
call_count = call_count + 1
return entry
end
local coros = {}
-- we're going to schedule a whole bunch of queries, all of this
-- function, which does the same lookup and stores the result
local x = function()
-- the function is ran when started. So we must immediately yield
-- so the scheduler loop can first schedule them all before actually
-- starting resolving
coroutine.yield(coroutine.running())
local _, _, _ = client.resolve(name, {qtype = client.TYPE_A})
end
-- schedule a bunch of the same lookups
for _ = 1, 10 do
local co = ngx.thread.spawn(x)
table.insert(coros, co)
end
-- all scheduled and waiting to start due to the yielding done.
-- now start them all
for i = 1, #coros do
ngx.thread.wait(coros[i]) -- this wait will resume the scheduled ones
end
-- all results are unique, each call got its own query
assert.equal(call_count, 10)
end)
end)