diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be63458b..17ca0705d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Fixed - Loading installed luarocks from outside rover [PR #503](https://github.com/3scale/apicast/pull/503) +- Support IPv6 addresses in `/etc/resolv.conf` [PR #511](https://github.com/3scale/apicast/pull/511) ## [3.2.0-alpha1] diff --git a/bin/busted b/bin/busted index be3171636..92a8cb907 100755 --- a/bin/busted +++ b/bin/busted @@ -14,6 +14,7 @@ chdir "$dirname/.."; my $apicast = getcwd(); exec '/usr/bin/env', 'resty', + '--errlog-level', 'alert', '--http-include', "$apicast/spec/fixtures/echo.conf", "$apicast/bin/busted.lua", '--config-file', "$apicast/.busted", diff --git a/gateway/src/resty/resolver.lua b/gateway/src/resty/resolver.lua index 4ff225a49..6a8c23fb4 100644 --- a/gateway/src/resty/resolver.lua +++ b/gateway/src/resty/resolver.lua @@ -8,12 +8,15 @@ local find = string.find local rep = string.rep local unpack = unpack local insert = table.insert -local getenv = os.getenv local concat = table.concat local io_type = io.type + +require('resty.core.regex') -- to allow use of ngx.re.match in the init phase + local re_match = ngx.re.match local resolver_cache = require 'resty.resolver.cache' local dns_client = require 'resty.resolver.dns_client' +local resty_env = require 'resty.env' local upstream = require 'ngx.upstream' local re = require('ngx.re') local semaphore = require "ngx.semaphore" @@ -53,6 +56,14 @@ local function read_resolv_conf(path) return output or "", err end +local function ipv4(address) + return re_match(address, '^([0-9]{1,3}\\.){3}[0-9]{1,3}$', 'oj') +end + +local function ipv6(address) + return re_match(address, '^\\[[a-f\\d:]+\\]$', 'oj') +end + local nameserver = { mt = { __tostring = function(t) @@ -62,9 +73,35 @@ local nameserver = { } function nameserver.new(host, port) + if not ipv4(host) and not ipv6(host) then + -- then it is likely ipv6 without [ ] around + host = format('[%s]', host) + end return setmetatable({ host, port or default_resolver_port }, nameserver.mt) end +function _M.parse_resolver(resolver) + if not resolver then return end + + local m, err = re_match(resolver, [[^ + ( + (?:\d{1,3}\.){3}\d{1,3} # ipv4 + | + \[[a-f\d:]+\] # ipv6 in [ ] brackes, like [dead::beef] + | + [a-f\d:]+ # ipv6 without brackets + ) + (?:\:(\d+))? # optional port + $]], 'ojx') + + if m then + return nameserver.new(m[1], m[2]) + else + return resolver, err or 'invalid address' + end +end + + function _M.parse_nameservers(path) local resolv_conf, err = read_resolv_conf(path) @@ -76,7 +113,6 @@ function _M.parse_nameservers(path) local search = { } local nameservers = { search = search } - local resolver = getenv('RESOLVER') local domains = match(resolv_conf, 'search%s+([^\n]+)') ngx.log(ngx.DEBUG, 'search ', domains) @@ -85,11 +121,15 @@ function _M.parse_nameservers(path) insert(search, domain) end - if resolver then - local m = re.split(resolver, ':', 'oj') - insert(nameservers, nameserver.new(m[1], m[2])) + local resolver + resolver, err = _M.parse_resolver(resty_env.value('RESOLVER')) + + if err then + ngx.log(ngx.ERR, 'invalid resolver ', resolver, ' error: ', err) + elseif resolver then -- we are going to use all resolvers, because we can't trust dnsmasq -- see https://github.com/3scale/apicast/issues/321 for more details + insert(nameservers, resolver) end for server in gmatch(resolv_conf, 'nameserver%s+([^%s]+)') do diff --git a/spec/resty/resolver_spec.lua b/spec/resty/resolver_spec.lua index fe1b2fedf..5e9a6dd3e 100644 --- a/spec/resty/resolver_spec.lua +++ b/spec/resty/resolver_spec.lua @@ -129,8 +129,36 @@ describe('resty.resolver', function() pending('does query when cached cname missing address') end) + describe('.parse_resolver', function() + + it("handles invalid data", function() + assert.same({'foobar', 'invalid address'}, { resty_resolver.parse_resolver('foobar') }) + end) + + it("handles missing data", function() + assert.same({}, { resty_resolver.parse_resolver() }) + end) + + it("parses ipv4 without port", function() + assert.same({'192.168.0.1', 53}, resty_resolver.parse_resolver('192.168.0.1')) + end) + + it("parses ipv4 with port", function() + assert.same({'192.168.0.1', '5353'}, resty_resolver.parse_resolver('192.168.0.1:5353')) + end) + + it("parses ipv6 without port", function() + assert.same({'[dead::beef:5353]', 53}, resty_resolver.parse_resolver('dead::beef:5353')) + end) + + it("parses ipv6 with port", function() + assert.same({'[dead::beef]', '5353'}, resty_resolver.parse_resolver('[dead::beef]:5353')) + end) + end) + describe('.parse_nameservers', function() local tmpname + local resty_env = require('resty.env') before_each(function() tmpname = io.tmpfile() @@ -155,6 +183,22 @@ describe('resty.resolver', function() assert.same({ 'localdomain.example.com', 'local' }, search) end) + it('ignores invalid RESOLVER', function() + resty_env.set('RESOLVER', 'invalid-nameserver') + + local nameservers = resty_resolver.parse_nameservers('') + + assert.equal(0, #nameservers) + end) + + it('uses correct RESOLVER', function() + resty_env.set('RESOLVER', '192.168.0.1:53') + + local nameservers = resty_resolver.parse_nameservers('') + + assert.equal(1, #nameservers) + assert.same({'192.168.0.1', '53'}, nameservers[1]) + end) end) end) diff --git a/t/resolver.t b/t/resolver.t index db74baf90..03b7112bc 100644 --- a/t/resolver.t +++ b/t/resolver.t @@ -2,13 +2,10 @@ use lib 't'; use TestAPIcast 'no_plan'; $ENV{TEST_NGINX_HTTP_CONFIG} = "$TestAPIcast::path/http.d/*.conf"; -$ENV{RESOLVER} = '127.0.1.1:5353'; +$ENV{TEST_NGINX_RESOLVER} = '127.0.1.1:5353'; $ENV{TEST_NGINX_RESOLV_CONF} = "$Test::Nginx::Util::HtmlDir/resolv.conf"; -env_to_nginx( - 'RESOLVER' -); master_on(); log_level('warn'); run_tests(); @@ -17,9 +14,11 @@ __DATA__ === TEST 1: uses all resolvers both RESOLVER env variable and resolvers in resolv.conf should be used +--- main_config +env RESOLVER=$TEST_NGINX_RESOLVER; --- http_config lua_package_path "$TEST_NGINX_LUA_PATH"; - init_by_lua_block { + init_worker_by_lua_block { require('resty.resolver').init('$TEST_NGINX_RESOLV_CONF') } --- config @@ -67,3 +66,27 @@ servers: 2 2.3.4.5:6789 --- no_error_log [error] + + +=== TEST 3: can have ipv6 RESOLVER +RESOLVER env variable can be IPv6 address +--- main_config +env RESOLVER=[dead::beef]:5353; +--- http_config + lua_package_path "$TEST_NGINX_LUA_PATH"; + init_worker_by_lua_block { + require('resty.resolver').init('$TEST_NGINX_RESOLV_CONF') + } +--- config + location = /t { + content_by_lua_block { + local nameservers = require('resty.resolver').nameservers() + ngx.say('nameservers: ', #nameservers, ' ', tostring(nameservers[1])) + } + } +--- request +GET /t +--- response_body +nameservers: 1 [dead::beef]:5353 +--- user_files +>>> resolv.conf