Merge branch 'master' of https://github.com/kubernetes/ingress-nginx into proxyssl

This commit is contained in:
Gabor Lekeny 2019-08-16 06:21:53 +02:00
commit 65b9e2c574
391 changed files with 23957 additions and 20447 deletions

View file

@ -24,7 +24,8 @@ RUN clean-install \
COPY --chown=www-data:www-data . /
RUN cp /usr/local/openresty/nginx/conf/mime.types /etc/nginx/mime.types
RUN cp /usr/local/openresty/nginx/conf/mime.types /etc/nginx/mime.types \
&& cp /usr/local/openresty/nginx/conf/fastcgi_params /etc/nginx/fastcgi_params
RUN ln -s /usr/local/openresty/nginx/modules /etc/nginx/modules
# Add LuaRocks paths

View file

@ -1,7 +1,7 @@
local ngx_balancer = require("ngx.balancer")
local cjson = require("cjson.safe")
local util = require("util")
local dns_util = require("util.dns")
local dns_lookup = require("util.dns").lookup
local configuration = require("configuration")
local round_robin = require("balancer.round_robin")
local chash = require("balancer.chash")
@ -52,7 +52,7 @@ local function resolve_external_names(original_backend)
local backend = util.deepcopy(original_backend)
local endpoints = {}
for _, endpoint in ipairs(backend.endpoints) do
local ips = dns_util.resolve(endpoint.address)
local ips = dns_lookup(endpoint.address)
for _, ip in ipairs(ips) do
table.insert(endpoints, { address = ip, port = endpoint.port })
end
@ -190,6 +190,10 @@ local function route_to_alternative_balancer(balancer)
end
local function get_balancer()
if ngx.ctx.balancer then
return ngx.ctx.balancer
end
local backend_name = ngx.var.proxy_upstream_name
local balancer = balancers[backend_name]
@ -201,9 +205,11 @@ local function get_balancer()
local alternative_backend_name = balancer.alternative_backends[1]
ngx.var.proxy_alternative_upstream_name = alternative_backend_name
return balancers[alternative_backend_name]
balancer = balancers[alternative_backend_name]
end
ngx.ctx.balancer = balancer
return balancer
end
@ -260,6 +266,7 @@ if _TEST then
_M.get_implementation = get_implementation
_M.sync_backend = sync_backend
_M.route_to_alternative_balancer = route_to_alternative_balancer
_M.get_balancer = get_balancer
end
return _M

View file

@ -5,6 +5,7 @@
-- /finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala
local resty_lock = require("resty.lock")
local util = require("util")
local split = require("util.split")
@ -13,10 +14,36 @@ local ngx_log = ngx.log
local INFO = ngx.INFO
local DECAY_TIME = 10 -- this value is in seconds
local LOCK_KEY = ":ewma_key"
local PICK_SET_SIZE = 2
local ewma_lock, ewma_lock_err = resty_lock:new("balancer_ewma_locks", {timeout = 0, exptime = 0.1})
if not ewma_lock then
error(ewma_lock_err)
end
local _M = { name = "ewma" }
local function lock(upstream)
local _, err = ewma_lock:lock(upstream .. LOCK_KEY)
if err then
if err ~= "timeout" then
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to lock: %s", tostring(err)))
end
end
return err
end
local function unlock()
local ok, err = ewma_lock:unlock()
if not ok then
ngx.log(ngx.ERR, string.format("EWMA Balancer failed to unlock: %s", tostring(err)))
end
return err
end
local function decay_ewma(ewma, last_touched_at, rtt, now)
local td = now - last_touched_at
td = (td > 0) and td or 0
@ -26,28 +53,55 @@ local function decay_ewma(ewma, last_touched_at, rtt, now)
return ewma
end
local function get_or_update_ewma(self, upstream, rtt, update)
local ewma = self.ewma[upstream] or 0
local function store_stats(upstream, ewma, now)
local success, err, forcible = ngx.shared.balancer_ewma_last_touched_at:set(upstream, now)
if not success then
ngx.log(ngx.WARN, "balancer_ewma_last_touched_at:set failed " .. err)
end
if forcible then
ngx.log(ngx.WARN, "balancer_ewma_last_touched_at:set valid items forcibly overwritten")
end
success, err, forcible = ngx.shared.balancer_ewma:set(upstream, ewma)
if not success then
ngx.log(ngx.WARN, "balancer_ewma:set failed " .. err)
end
if forcible then
ngx.log(ngx.WARN, "balancer_ewma:set valid items forcibly overwritten")
end
end
local function get_or_update_ewma(upstream, rtt, update)
local lock_err = nil
if update then
lock_err = lock(upstream)
end
local ewma = ngx.shared.balancer_ewma:get(upstream) or 0
if lock_err ~= nil then
return ewma, lock_err
end
local now = ngx.now()
local last_touched_at = self.ewma_last_touched_at[upstream] or 0
local last_touched_at = ngx.shared.balancer_ewma_last_touched_at:get(upstream) or 0
ewma = decay_ewma(ewma, last_touched_at, rtt, now)
if not update then
return ewma, nil
end
self.ewma[upstream] = ewma
self.ewma_last_touched_at[upstream] = now
store_stats(upstream, ewma, now)
unlock()
return ewma, nil
end
local function score(self, upstream)
local function score(upstream)
-- Original implementation used names
-- Endpoints don't have names, so passing in IP:Port as key instead
local upstream_name = upstream.address .. ":" .. upstream.port
return get_or_update_ewma(self, upstream_name, 0, false)
return get_or_update_ewma(upstream_name, 0, false)
end
-- implementation similar to https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
@ -63,12 +117,12 @@ local function shuffle_peers(peers, k)
-- peers[1 .. k] will now contain a randomly selected k from #peers
end
local function pick_and_score(self, peers, k)
local function pick_and_score(peers, k)
shuffle_peers(peers, k)
local lowest_score_index = 1
local lowest_score = score(self, peers[lowest_score_index])
local lowest_score = score(peers[lowest_score_index])
for i = 2, k do
local new_score = score(self, peers[i])
local new_score = score(peers[i])
if new_score < lowest_score then
lowest_score_index, lowest_score = i, new_score
end
@ -76,6 +130,31 @@ local function pick_and_score(self, peers, k)
return peers[lowest_score_index], lowest_score
end
-- slow_start_ewma is something we use to avoid sending too many requests
-- to the newly introduced endpoints. We currently use average ewma values
-- of existing endpoints.
local function calculate_slow_start_ewma(self)
local total_ewma = 0
local endpoints_count = 0
for _, endpoint in pairs(self.peers) do
local endpoint_string = endpoint.address .. ":" .. endpoint.port
local ewma = ngx.shared.balancer_ewma:get(endpoint_string)
if ewma then
endpoints_count = endpoints_count + 1
total_ewma = total_ewma + ewma
end
end
if endpoints_count == 0 then
ngx.log(ngx.INFO, "no ewma value exists for the endpoints")
return nil
end
return total_ewma / endpoints_count
end
function _M.balance(self)
local peers = self.peers
local endpoint, ewma_score = peers[1], -1
@ -83,7 +162,7 @@ function _M.balance(self)
if #peers > 1 then
local k = (#peers < PICK_SET_SIZE) and #peers or PICK_SET_SIZE
local peer_copy = util.deepcopy(peers)
endpoint, ewma_score = pick_and_score(self, peer_copy, k)
endpoint, ewma_score = pick_and_score(peer_copy, k)
end
ngx.var.balancer_ewma_score = ewma_score
@ -92,7 +171,7 @@ function _M.balance(self)
return endpoint.address .. ":" .. endpoint.port
end
function _M.after_balance(self)
function _M.after_balance(_)
local response_time = tonumber(split.get_first_value(ngx.var.upstream_response_time)) or 0
local connect_time = tonumber(split.get_first_value(ngx.var.upstream_connect_time)) or 0
local rtt = connect_time + response_time
@ -101,30 +180,41 @@ function _M.after_balance(self)
if util.is_blank(upstream) then
return
end
get_or_update_ewma(self, upstream, rtt, true)
get_or_update_ewma(upstream, rtt, true)
end
function _M.sync(self, backend)
self.traffic_shaping_policy = backend.trafficShapingPolicy
self.alternative_backends = backend.alternativeBackends
local normalized_endpoints_added, normalized_endpoints_removed = util.diff_endpoints(self.peers, backend.endpoints)
local changed = not util.deep_compare(self.peers, backend.endpoints)
if not changed then
if #normalized_endpoints_added == 0 and #normalized_endpoints_removed == 0 then
ngx.log(ngx.INFO, "endpoints did not change for backend " .. tostring(backend.name))
return
end
ngx_log(INFO, string_format("[%s] peers have changed for backend %s", self.name, backend.name))
self.traffic_shaping_policy = backend.trafficShapingPolicy
self.alternative_backends = backend.alternativeBackends
self.peers = backend.endpoints
self.ewma = {}
self.ewma_last_touched_at = {}
for _, endpoint_string in ipairs(normalized_endpoints_removed) do
ngx.shared.balancer_ewma:delete(endpoint_string)
ngx.shared.balancer_ewma_last_touched_at:delete(endpoint_string)
end
local slow_start_ewma = calculate_slow_start_ewma(self)
if slow_start_ewma ~= nil then
local now = ngx.now()
for _, endpoint_string in ipairs(normalized_endpoints_added) do
store_stats(endpoint_string, slow_start_ewma, now)
end
end
end
function _M.new(self, backend)
local o = {
peers = backend.endpoints,
ewma = {},
ewma_last_touched_at = {},
traffic_shaping_policy = backend.trafficShapingPolicy,
alternative_backends = backend.alternativeBackends,
}

View file

@ -4,9 +4,7 @@ local cjson = require("cjson.safe")
local configuration_data = ngx.shared.configuration_data
local certificate_data = ngx.shared.certificate_data
local _M = {
nameservers = {}
}
local _M = {}
function _M.get_backends_data()
return configuration_data:get("backends")

View file

@ -1,7 +1,7 @@
local ngx_balancer = require("ngx.balancer")
local cjson = require("cjson.safe")
local util = require("util")
local dns_util = require("util.dns")
local dns_lookup = require("util.dns").lookup
local configuration = require("tcp_udp_configuration")
local round_robin = require("balancer.round_robin")
@ -34,7 +34,7 @@ local function resolve_external_names(original_backend)
local backend = util.deepcopy(original_backend)
local endpoints = {}
for _, endpoint in ipairs(backend.endpoints) do
local ips = dns_util.resolve(endpoint.address)
local ips = dns_lookup(endpoint.address)
for _, ip in ipairs(ips) do
table.insert(endpoints, {address = ip, port = endpoint.port})
end

View file

@ -1,91 +1,151 @@
local util = require("util")
local original_ngx = ngx
local function reset_ngx()
_G.ngx = original_ngx
end
local function mock_ngx(mock)
local _ngx = mock
setmetatable(_ngx, { __index = ngx })
_G.ngx = _ngx
end
local function flush_all_ewma_stats()
ngx.shared.balancer_ewma:flush_all()
ngx.shared.balancer_ewma_last_touched_at:flush_all()
end
local function store_ewma_stats(endpoint_string, ewma, touched_at)
ngx.shared.balancer_ewma:set(endpoint_string, ewma)
ngx.shared.balancer_ewma_last_touched_at:set(endpoint_string, touched_at)
end
local function assert_ewma_stats(endpoint_string, ewma, touched_at)
assert.are.equals(ewma, ngx.shared.balancer_ewma:get(endpoint_string))
assert.are.equals(touched_at, ngx.shared.balancer_ewma_last_touched_at:get(endpoint_string))
end
describe("Balancer ewma", function()
local balancer_ewma = require("balancer.ewma")
local ngx_now = 1543238266
local backend, instance
before_each(function()
mock_ngx({ now = function() return ngx_now end, var = { balancer_ewma_score = -1 } })
backend = {
name = "namespace-service-port", ["load-balance"] = "ewma",
endpoints = {
{ address = "10.10.10.1", port = "8080", maxFails = 0, failTimeout = 0 },
{ address = "10.10.10.2", port = "8080", maxFails = 0, failTimeout = 0 },
{ address = "10.10.10.3", port = "8080", maxFails = 0, failTimeout = 0 },
}
}
store_ewma_stats("10.10.10.1:8080", 0.2, ngx_now - 1)
store_ewma_stats("10.10.10.2:8080", 0.3, ngx_now - 5)
store_ewma_stats("10.10.10.3:8080", 1.2, ngx_now - 20)
instance = balancer_ewma:new(backend)
end)
after_each(function()
reset_ngx()
flush_all_ewma_stats()
end)
describe("after_balance()", function()
local ngx_now = 1543238266
_G.ngx.now = function() return ngx_now end
_G.ngx.var = { upstream_response_time = "0.25", upstream_connect_time = "0.02", upstream_addr = "10.184.7.40:8080" }
it("updates EWMA stats", function()
local backend = {
name = "my-dummy-backend", ["load-balance"] = "ewma",
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } }
}
local instance = balancer_ewma:new(backend)
ngx.var = { upstream_addr = "10.10.10.2:8080", upstream_connect_time = "0.02", upstream_response_time = "0.1" }
instance:after_balance()
assert.equal(0.27, instance.ewma[ngx.var.upstream_addr])
assert.equal(ngx_now, instance.ewma_last_touched_at[ngx.var.upstream_addr])
local weight = math.exp(-5 / 10)
local expected_ewma = 0.3 * weight + 0.12 * (1.0 - weight)
assert.are.equals(expected_ewma, ngx.shared.balancer_ewma:get(ngx.var.upstream_addr))
assert.are.equals(ngx_now, ngx.shared.balancer_ewma_last_touched_at:get(ngx.var.upstream_addr))
end)
end)
describe("balance()", function()
it("returns single endpoint when the given backend has only one endpoint", function()
local backend = {
name = "my-dummy-backend", ["load-balance"] = "ewma",
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } }
}
local instance = balancer_ewma:new(backend)
local single_endpoint_backend = util.deepcopy(backend)
table.remove(single_endpoint_backend.endpoints, 3)
table.remove(single_endpoint_backend.endpoints, 2)
local single_endpoint_instance = balancer_ewma:new(single_endpoint_backend)
local peer = instance:balance()
assert.equal("10.184.7.40:8080", peer)
local peer = single_endpoint_instance:balance()
assert.are.equals("10.10.10.1:8080", peer)
assert.are.equals(-1, ngx.var.balancer_ewma_score)
end)
it("picks the endpoint with lowest score when there two of them", function()
local backend = {
name = "my-dummy-backend", ["load-balance"] = "ewma",
endpoints = {
{ address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 },
{ address = "10.184.97.100", port = "8080", maxFails = 0, failTimeout = 0 },
}
}
local instance = balancer_ewma:new(backend)
instance.ewma = { ["10.184.7.40:8080"] = 0.5, ["10.184.97.100:8080"] = 0.3 }
instance.ewma_last_touched_at = { ["10.184.7.40:8080"] = ngx.now(), ["10.184.97.100:8080"] = ngx.now() }
it("picks the endpoint with lowest decayed score", function()
local two_endpoints_backend = util.deepcopy(backend)
table.remove(two_endpoints_backend.endpoints, 2)
local two_endpoints_instance = balancer_ewma:new(two_endpoints_backend)
local peer = instance:balance()
assert.equal("10.184.97.100:8080", peer)
local peer = two_endpoints_instance:balance()
-- even though 10.10.10.1:8080 has a lower ewma score
-- algorithm picks 10.10.10.3:8080 because its decayed score is even lower
assert.equal("10.10.10.3:8080", peer)
assert.are.equals(0.16240233988393523723, ngx.var.balancer_ewma_score)
end)
end)
describe("sync()", function()
local backend, instance
before_each(function()
backend = {
name = "my-dummy-backend", ["load-balance"] = "ewma",
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } }
}
instance = balancer_ewma:new(backend)
end)
it("does nothing when endpoints do not change", function()
local new_backend = {
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } }
}
instance:sync(new_backend)
end)
it("updates endpoints", function()
local new_backend = {
endpoints = {
{ address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 },
{ address = "10.184.97.100", port = "8080", maxFails = 0, failTimeout = 0 },
}
}
instance:sync(new_backend)
assert.are.same(new_backend.endpoints, instance.peers)
end)
it("resets stats", function()
it("does not reset stats when endpoints do not change", function()
local new_backend = util.deepcopy(backend)
new_backend.endpoints[1].maxFails = 3
instance:sync(new_backend)
assert.are.same(new_backend.endpoints, instance.peers)
assert_ewma_stats("10.10.10.1:8080", 0.2, ngx_now - 1)
assert_ewma_stats("10.10.10.2:8080", 0.3, ngx_now - 5)
assert_ewma_stats("10.10.10.3:8080", 1.2, ngx_now - 20)
end)
it("updates peers, deletes stats for old endpoints and sets average ewma score to new ones", function()
local new_backend = util.deepcopy(backend)
-- existing endpoint 10.10.10.2 got deleted
-- and replaced with 10.10.10.4
new_backend.endpoints[2].address = "10.10.10.4"
-- and there's one new extra endpoint
table.insert(new_backend.endpoints, { address = "10.10.10.5", port = "8080", maxFails = 0, failTimeout = 0 })
instance:sync(new_backend)
assert.are.same(new_backend.endpoints, instance.peers)
assert_ewma_stats("10.10.10.1:8080", 0.2, ngx_now - 1)
assert_ewma_stats("10.10.10.2:8080", nil, nil)
assert_ewma_stats("10.10.10.3:8080", 1.2, ngx_now - 20)
local slow_start_ewma = (0.2 + 1.2) / 2
assert_ewma_stats("10.10.10.4:8080", slow_start_ewma, ngx_now)
assert_ewma_stats("10.10.10.5:8080", slow_start_ewma, ngx_now)
end)
it("does not set slow_start_ewma when there is no existing ewma", function()
local new_backend = util.deepcopy(backend)
table.insert(new_backend.endpoints, { address = "10.10.10.4", port = "8080", maxFails = 0, failTimeout = 0 })
-- when the LB algorithm instance is just instantiated it won't have any
-- ewma value set for the initial endpoints (because it has not processed any request yet),
-- this test is trying to simulate that by flushing existing ewma values
flush_all_ewma_stats()
instance:sync(new_backend)
assert_ewma_stats("10.10.10.1:8080", nil, nil)
assert_ewma_stats("10.10.10.2:8080", nil, nil)
assert_ewma_stats("10.10.10.3:8080", nil, nil)
assert_ewma_stats("10.10.10.4:8080", nil, nil)
end)
end)
end)

View file

@ -33,7 +33,7 @@ local function reset_backends()
backends = {
{
name = "access-router-production-web-80", port = "80", secure = false,
secureCACert = { secret = "", caFilename = "", pemSha = "" },
secureCACert = { secret = "", caFilename = "", caSha = "" },
sslPassthrough = false,
endpoints = {
{ address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 },
@ -49,7 +49,7 @@ local function reset_backends()
},
},
{ name = "my-dummy-app-1", ["load-balance"] = "round_robin", },
{
{
name = "my-dummy-app-2", ["load-balance"] = "chash",
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
},
@ -86,6 +86,44 @@ describe("Balancer", function()
end)
end)
describe("get_balancer()", function()
it("always returns the same balancer for given request context", function()
local backend = {
name = "my-dummy-app-6", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-6" },
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
trafficShapingPolicy = {
weight = 0,
header = "",
headerValue = "",
cookie = ""
},
}
local canary_backend = {
name = "my-dummy-canary-app-6", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-6" },
endpoints = { { address = "11.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
trafficShapingPolicy = {
weight = 5,
header = "",
headerValue = "",
cookie = ""
},
}
balancer.sync_backend(backend)
balancer.sync_backend(canary_backend)
mock_ngx({ var = { proxy_upstream_name = backend.name } })
local expected = balancer.get_balancer()
for i = 1,50,1 do
assert.are.same(expected, balancer.get_balancer())
end
end)
end)
describe("route_to_alternative_balancer()", function()
local backend, _balancer
@ -277,8 +315,7 @@ describe("Balancer", function()
}
}
local dns_helper = require("test/dns_helper")
dns_helper.mock_dns_query({
helpers.mock_resty_dns_query(nil, {
{
name = "example.com",
address = "192.168.1.1",

View file

@ -115,15 +115,20 @@ describe("Configuration", function()
end)
it("returns a status of 400", function()
local original_io_open = _G.io.open
_G.io.open = function(filename, extension) return false end
assert.has_no.errors(configuration.call)
assert.equal(ngx.status, ngx.HTTP_BAD_REQUEST)
_G.io.open = original_io_open
end)
it("logs 'dynamic-configuration: unable to read valid request body to stderr'", function()
local original_io_open = _G.io.open
_G.io.open = function(filename, extension) return false end
local s = spy.on(ngx, "log")
assert.has_no.errors(configuration.call)
assert.spy(s).was_called_with(ngx.ERR, "dynamic-configuration: unable to read valid request body")
_G.io.open = original_io_open
end)
end)

View file

@ -1,27 +0,0 @@
local _M = {}
local configuration = require("configuration")
local resolver = require("resty.dns.resolver")
local old_resolver_new = resolver.new
local function reset(nameservers)
configuration.nameservers = nameservers or { "1.1.1.1" }
end
function _M.mock_new(func, nameservers)
reset(nameservers)
resolver.new = func
end
function _M.mock_dns_query(response, err)
reset()
resolver.new = function(self, options)
local r = old_resolver_new(self, options)
r.query = function(self, name, options, tries)
return response, err
end
return r
end
end
return _M

View file

@ -0,0 +1,50 @@
local _M = {}
local resty_dns_resolver = require("resty.dns.resolver")
local original_resty_dns_resolver_new = resty_dns_resolver.new
local original_io_open = io.open
function _M.with_resolv_conf(content, func)
local new_resolv_conf_f = assert(io.tmpfile())
new_resolv_conf_f:write(content)
new_resolv_conf_f:seek("set", 0)
io.open = function(path, mode)
if path ~= "/etc/resolv.conf" then
error("expected '/etc/resolv.conf' as path but got: " .. tostring(path))
end
if mode ~= "r" then
error("expected 'r' as mode but got: " .. tostring(mode))
end
return new_resolv_conf_f, nil
end
func()
io.open = original_io_open
if io.type(new_resolv_conf_f) ~= "closed file" then
error("file was left open")
end
end
function _M.mock_resty_dns_new(func)
resty_dns_resolver.new = func
end
function _M.mock_resty_dns_query(mocked_host, response, err)
resty_dns_resolver.new = function(self, options)
local r = original_resty_dns_resolver_new(self, options)
r.query = function(self, host, options, tries)
if mocked_host and mocked_host ~= host then
return error(tostring(host) .. " is not mocked")
end
return response, err
end
return r
end
end
return _M

View file

@ -11,6 +11,7 @@ do
-- if there's more constants need to be whitelisted for test runs, add here.
local GLOBALS_ALLOWED_IN_TEST = {
_TEST = true,
helpers = true,
}
local newindex = function(table, key, value)
rawset(table, key, value)
@ -33,6 +34,8 @@ do
setmetatable(_G, { __newindex = newindex })
end
_G.helpers = require("test.helpers")
_G._TEST = true
local ffi = require("ffi")
local lua_ingress = require("lua_ingress")

View file

@ -1,43 +1,89 @@
describe("resolve", function()
local dns = require("util.dns")
local dns_helper = require("test/dns_helper")
local conf = [===[
nameserver 1.2.3.4
nameserver 4.5.6.7
search ingress-nginx.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
]===]
helpers.with_resolv_conf(conf, function()
require("util.resolv_conf")
end)
describe("dns.lookup", function()
local dns, dns_lookup, spy_ngx_log
before_each(function()
spy_ngx_log = spy.on(ngx, "log")
dns = require("util.dns")
dns_lookup = dns.lookup
end)
after_each(function()
package.loaded["util.dns"] = nil
end)
it("sets correct nameservers", function()
dns_helper.mock_new(function(self, options)
helpers.mock_resty_dns_new(function(self, options)
assert.are.same({ nameservers = { "1.2.3.4", "4.5.6.7" }, retrans = 5, timeout = 2000 }, options)
return nil, ""
end, { "1.2.3.4", "4.5.6.7" })
dns.resolve("example.com")
end)
dns_lookup("example.com")
end)
it("returns host when an error happens", function()
local s_ngx_log = spy.on(ngx, "log")
describe("when there's an error", function()
it("returns host when resolver can not be instantiated", function()
helpers.mock_resty_dns_new(function(...) return nil, "an error" end)
assert.are.same({ "example.com" }, dns_lookup("example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to instantiate the resolver: an error")
end)
dns_helper.mock_new(function(...) return nil, "an error" end)
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to instantiate the resolver: an error")
it("returns host when the query returns nil", function()
helpers.mock_resty_dns_query(nil, nil, "oops!")
assert.are.same({ "example.com" }, dns_lookup("example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\noops!\noops!")
end)
dns_helper.mock_dns_query(nil, "oops!")
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\noops!\noops!")
it("returns host when the query returns empty answer", function()
helpers.mock_resty_dns_query(nil, {})
assert.are.same({ "example.com" }, dns_lookup("example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno A record resolved\nno AAAA record resolved")
end)
dns_helper.mock_dns_query({ errcode = 1, errstr = "format error" })
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nserver returned error code: 1: format error\nserver returned error code: 1: format error")
it("returns host when there's answer but with error", function()
helpers.mock_resty_dns_query(nil, { errcode = 1, errstr = "format error" })
assert.are.same({ "example.com" }, dns_lookup("example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\n" ..
"server returned error code: 1: format error\nserver returned error code: 1: format error")
end)
dns_helper.mock_dns_query({})
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno record resolved\nno record resolved")
it("retuns host when there's answer but no A/AAAA record in it", function()
helpers.mock_resty_dns_query(nil, { { name = "example.com", cname = "sub.example.com", ttl = 60 } })
assert.are.same({ "example.com" }, dns_lookup("example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno A record resolved\nno AAAA record resolved")
end)
dns_helper.mock_dns_query({ { name = "example.com", cname = "sub.example.com", ttl = 60 } })
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno record resolved\nno record resolved")
it("returns host when the query returns nil and number of dots is not less than configured ndots", function()
helpers.mock_resty_dns_query(nil, nil, "oops!")
assert.are.same({ "a.b.c.d.example.com" }, dns_lookup("a.b.c.d.example.com"))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\noops!\noops!")
end)
it("returns host when the query returns nil for a fully qualified domain", function()
helpers.mock_resty_dns_query("example.com.", nil, "oops!")
assert.are.same({ "example.com." }, dns_lookup("example.com."))
assert.spy(spy_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\noops!\noops!")
end)
end)
it("resolves all A records of given host, caches them with minimal ttl and returns from cache next time", function()
dns_helper.mock_dns_query({
it("returns answer from cache if it exists without doing actual DNS query", function()
dns._cache:set("example.com", { "192.168.1.1" })
assert.are.same({ "192.168.1.1" }, dns_lookup("example.com"))
end)
it("resolves a fully qualified domain without looking at resolv.conf search and caches result", function()
helpers.mock_resty_dns_query("example.com.", {
{
name = "example.com",
name = "example.com.",
address = "192.168.1.1",
ttl = 3600,
},
@ -47,28 +93,43 @@ describe("resolve", function()
ttl = 60,
}
})
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns_lookup("example.com."))
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns._cache:get("example.com."))
end)
local lrucache = require("resty.lrucache")
local old_lrucache_new = lrucache.new
lrucache.new = function(...)
local cache = old_lrucache_new(...)
it("starts with host itself when number of dots is not less than configured ndots", function()
local host = "a.b.c.d.example.com"
helpers.mock_resty_dns_query(host, { { name = host, address = "192.168.1.1", ttl = 3600, } } )
local old_set = cache.set
cache.set = function(self, key, value, ttl)
assert.equal("example.com", key)
assert.are.same({ "192.168.1.1", "1.2.3.4" }, value)
assert.equal(60, ttl)
return old_set(self, key, value, ttl)
end
assert.are.same({ "192.168.1.1" }, dns_lookup(host))
assert.are.same({ "192.168.1.1" }, dns._cache:get(host))
end)
return cache
end
it("starts with first search entry when number of dots is less than configured ndots", function()
local host = "example.com.ingress-nginx.svc.cluster.local"
helpers.mock_resty_dns_query(host, { { name = host, address = "192.168.1.1", ttl = 3600, } } )
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns.resolve("example.com"))
assert.are.same({ "192.168.1.1" }, dns_lookup(host))
assert.are.same({ "192.168.1.1" }, dns._cache:get(host))
end)
dns_helper.mock_new(function(...)
error("expected to short-circuit and return response from cache")
end)
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns.resolve("example.com"))
it("it caches with minimal ttl", function()
helpers.mock_resty_dns_query("example.com.", {
{
name = "example.com.",
address = "192.168.1.1",
ttl = 3600,
},
{
name = "example.com.",
address = "1.2.3.4",
ttl = 60,
}
})
local spy_cache_set = spy.on(dns._cache, "set")
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns_lookup("example.com."))
assert.spy(spy_cache_set).was_called_with(match.is_table(), "example.com.", { "192.168.1.1", "1.2.3.4" }, 60)
end)
end)

View file

@ -0,0 +1,64 @@
local original_io_open = io.open
describe("resolv_conf", function()
after_each(function()
package.loaded["util.resolv_conf"] = nil
io.open = original_io_open
end)
it("errors when file can not be opened", function()
io.open = function(...)
return nil, "file does not exist"
end
assert.has_error(function() require("util.resolv_conf") end, "could not open /etc/resolv.conf: file does not exist")
end)
it("opens '/etc/resolv.conf' with mode 'r'", function()
io.open = function(path, mode)
assert.are.same("/etc/resolv.conf", path)
assert.are.same("r", mode)
return original_io_open(path, mode)
end
assert.has_no.errors(function() require("util.resolv_conf") end)
end)
it("correctly parses resolv.conf", function()
local conf = [===[
# This is a comment
nameserver 10.96.0.10
nameserver 10.96.0.99
search ingress-nginx.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
]===]
helpers.with_resolv_conf(conf, function()
local resolv_conf = require("util.resolv_conf")
assert.are.same({
nameservers = { "10.96.0.10", "10.96.0.99" },
search = { "ingress-nginx.svc.cluster.local", "svc.cluster.local", "cluster.local" },
ndots = 5,
}, resolv_conf)
end)
end)
it("ignores options that it does not understand", function()
local conf = [===[
nameserver 10.96.0.10
search example.com
options debug
options ndots:3
]===]
helpers.with_resolv_conf(conf, function()
local resolv_conf = require("util.resolv_conf")
assert.are.same({
nameservers = { "10.96.0.10" },
search = { "example.com" },
ndots = 3,
}, resolv_conf)
end)
end)
end)

View file

@ -12,24 +12,87 @@ end
describe("lua_ngx_var", function()
local util = require("util")
before_each(function()
mock_ngx({ var = { remote_addr = "192.168.1.1", [1] = "nginx/regexp/1/group/capturing" } })
end)
after_each(function()
reset_ngx()
package.loaded["monitor"] = nil
end)
it("returns value of nginx var by key", function()
assert.equal("192.168.1.1", util.lua_ngx_var("$remote_addr"))
describe("lua_ngx_var", function()
before_each(function()
mock_ngx({ var = { remote_addr = "192.168.1.1", [1] = "nginx/regexp/1/group/capturing" } })
end)
it("returns value of nginx var by key", function()
assert.equal("192.168.1.1", util.lua_ngx_var("$remote_addr"))
end)
it("returns value of nginx var when key is number", function()
assert.equal("nginx/regexp/1/group/capturing", util.lua_ngx_var("$1"))
end)
it("returns nil when variable is not defined", function()
assert.equal(nil, util.lua_ngx_var("$foo_bar"))
end)
end)
it("returns value of nginx var when key is number", function()
assert.equal("nginx/regexp/1/group/capturing", util.lua_ngx_var("$1"))
end)
describe("diff_endpoints", function()
it("returns removed and added endpoints", function()
local old = {
{ address = "10.10.10.1", port = "8080" },
{ address = "10.10.10.2", port = "8080" },
{ address = "10.10.10.3", port = "8080" },
}
local new = {
{ address = "10.10.10.1", port = "8080" },
{ address = "10.10.10.2", port = "8081" },
{ address = "11.10.10.2", port = "8080" },
{ address = "11.10.10.3", port = "8080" },
}
local expected_added = { "10.10.10.2:8081", "11.10.10.2:8080", "11.10.10.3:8080" }
table.sort(expected_added)
local expected_removed = { "10.10.10.2:8080", "10.10.10.3:8080" }
table.sort(expected_removed)
it("returns nil when variable is not defined", function()
assert.equal(nil, util.lua_ngx_var("$foo_bar"))
local added, removed = util.diff_endpoints(old, new)
table.sort(added)
table.sort(removed)
assert.are.same(expected_added, added)
assert.are.same(expected_removed, removed)
end)
it("returns empty results for empty inputs", function()
local added, removed = util.diff_endpoints({}, {})
assert.are.same({}, added)
assert.are.same({}, removed)
end)
it("returns empty results for same inputs", function()
local old = {
{ address = "10.10.10.1", port = "8080" },
{ address = "10.10.10.2", port = "8080" },
{ address = "10.10.10.3", port = "8080" },
}
local new = util.deepcopy(old)
local added, removed = util.diff_endpoints(old, new)
assert.are.same({}, added)
assert.are.same({}, removed)
end)
it("handles endpoints with nil attribute", function()
local old = {
{ address = nil, port = "8080" },
{ address = "10.10.10.2", port = "8080" },
{ address = "10.10.10.3", port = "8080" },
}
local new = util.deepcopy(old)
new[2].port = nil
local added, removed = util.diff_endpoints(old, new)
assert.are.same({ "10.10.10.2:nil" }, added)
assert.are.same({ "10.10.10.2:8080" }, removed)
end)
end)
end)

View file

@ -1,5 +1,6 @@
local string_len = string.len
local string_sub = string.sub
local string_format = string.format
local _M = {}
@ -26,6 +27,44 @@ function _M.lua_ngx_var(ngx_var)
return ngx.var[var_name]
end
-- normalize_endpoints takes endpoints as an array of endpoint objects
-- and returns a table where keys are string that's endpoint.address .. ":" .. endpoint.port
-- and values are all true
local function normalize_endpoints(endpoints)
local normalized_endpoints = {}
for _, endpoint in pairs(endpoints) do
local endpoint_string = string_format("%s:%s", endpoint.address, endpoint.port)
normalized_endpoints[endpoint_string] = true
end
return normalized_endpoints
end
-- diff_endpoints compares old and new
-- and as a first argument returns what endpoints are in new
-- but are not in old, and as a second argument it returns
-- what endpoints are in old but are in new.
-- Both return values are normalized (ip:port).
function _M.diff_endpoints(old, new)
local endpoints_added, endpoints_removed = {}, {}
local normalized_old, normalized_new = normalize_endpoints(old), normalize_endpoints(new)
for endpoint_string, _ in pairs(normalized_old) do
if not normalized_new[endpoint_string] then
table.insert(endpoints_removed, endpoint_string)
end
end
for endpoint_string, _ in pairs(normalized_new) do
if not normalized_old[endpoint_string] then
table.insert(endpoints_added, endpoint_string)
end
end
return endpoints_added, endpoints_removed
end
-- this implementation is taken from
-- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3
-- and modified for use in this project

View file

@ -1,25 +1,49 @@
local resolver = require("resty.dns.resolver")
local lrucache = require("resty.lrucache")
local configuration = require("configuration")
local util = require("util")
local resolv_conf = require("util.resolv_conf")
local ngx_log = ngx.log
local ngx_INFO = ngx.INFO
local ngx_ERR = ngx.ERR
local string_format = string.format
local table_concat = table.concat
local table_insert = table.insert
local ipairs = ipairs
local tostring = tostring
local _M = {}
local CACHE_SIZE = 10000
local MAXIMUM_TTL_VALUE = 2147483647 -- maximum value according to https://tools.ietf.org/html/rfc2181
-- for every host we will try two queries for the following types with the order set here
local QTYPES_TO_CHECK = { resolver.TYPE_A, resolver.TYPE_AAAA }
local cache, err = lrucache.new(CACHE_SIZE)
if not cache then
return error("failed to create the cache: " .. (err or "unknown"))
local cache
do
local err
cache, err = lrucache.new(CACHE_SIZE)
if not cache then
return error("failed to create the cache: " .. (err or "unknown"))
end
end
local function a_records_and_max_ttl(answers)
local function cache_set(host, addresses, ttl)
cache:set(host, addresses, ttl)
ngx_log(ngx_INFO, string_format("cache set for '%s' with value of [%s] and ttl of %s.",
host, table_concat(addresses, ", "), ttl))
end
local function is_fully_qualified(host)
return host:sub(-1) == "."
end
local function a_records_and_min_ttl(answers)
local addresses = {}
local ttl = MAXIMUM_TTL_VALUE -- maximum value according to https://tools.ietf.org/html/rfc2181
for _, ans in ipairs(answers) do
if ans.address then
table.insert(addresses, ans.address)
if ttl > ans.ttl then
table_insert(addresses, ans.address)
if ans.ttl < ttl then
ttl = ans.ttl
end
end
@ -28,71 +52,109 @@ local function a_records_and_max_ttl(answers)
return addresses, ttl
end
local function resolve_host(host, r, qtype)
local answers
answers, err = r:query(host, { qtype = qtype }, {})
local function resolve_host_for_qtype(r, host, qtype)
ngx_log(ngx_INFO, string_format("resolving %s with qtype %s", host, qtype))
local answers, err = r:query(host, { qtype = qtype }, {})
if not answers then
return nil, -1, tostring(err)
return nil, -1, err
end
if answers.errcode then
return nil, -1, string.format("server returned error code: %s: %s", answers.errcode, answers.errstr)
return nil, -1, string_format("server returned error code: %s: %s", answers.errcode, answers.errstr)
end
local addresses, ttl = a_records_and_max_ttl(answers)
local addresses, ttl = a_records_and_min_ttl(answers)
if #addresses == 0 then
return nil, -1, "no record resolved"
local msg = "no A record resolved"
if qtype == resolver.TYPE_AAAA then msg = "no AAAA record resolved" end
return nil, -1, msg
end
return addresses, ttl, nil
end
function _M.resolve(host)
local function resolve_host(r, host)
local dns_errors = {}
for _, qtype in ipairs(QTYPES_TO_CHECK) do
local addresses, ttl, err = resolve_host_for_qtype(r, host, qtype)
if addresses and #addresses > 0 then
return addresses, ttl, nil
end
table_insert(dns_errors, tostring(err))
end
return nil, nil, dns_errors
end
function _M.lookup(host)
local cached_addresses = cache:get(host)
if cached_addresses then
local message = string.format(
"addresses %s for host %s was resolved from cache",
table.concat(cached_addresses, ", "), host)
ngx.log(ngx.INFO, message)
return cached_addresses
end
local r
r, err = resolver:new{
nameservers = util.deepcopy(configuration.nameservers),
local r, err = resolver:new{
nameservers = resolv_conf.nameservers,
retrans = 5,
timeout = 2000, -- 2 sec
}
if not r then
ngx.log(ngx.ERR, "failed to instantiate the resolver: " .. tostring(err))
ngx_log(ngx_ERR, string_format("failed to instantiate the resolver: %s", err))
return { host }
end
local dns_errors = {}
local addresses, ttl, dns_errors
local addresses, ttl
addresses, ttl, err = resolve_host(host, r, r.TYPE_A)
if not addresses then
table.insert(dns_errors, tostring(err))
elseif #addresses > 0 then
cache:set(host, addresses, ttl)
return addresses
-- when the queried domain is fully qualified
-- then we don't go through resolv_conf.search
-- NOTE(elvinefendi): currently FQDN as externalName will be supported starting
-- with K8s 1.15: https://github.com/kubernetes/kubernetes/pull/78385
if is_fully_qualified(host) then
addresses, ttl, dns_errors = resolve_host(r, host)
if addresses then
cache_set(host, addresses, ttl)
return addresses
end
ngx_log(ngx_ERR, "failed to query the DNS server:\n" .. table_concat(dns_errors, "\n"))
return { host }
end
addresses, ttl, err = resolve_host(host, r, r.TYPE_AAAA)
if not addresses then
table.insert(dns_errors, tostring(err))
elseif #addresses > 0 then
cache:set(host, addresses, ttl)
return addresses
-- for non fully qualified domains if number of dots in
-- the queried host is less than resolv_conf.ndots then we try
-- with all the entries in resolv_conf.search before trying the original host
--
-- if number of dots is not less than resolv_conf.ndots then we start with
-- the original host and then try entries in resolv_conf.search
local _, host_ndots = host:gsub("%.", "")
local search_start, search_end = 0, #resolv_conf.search
if host_ndots < resolv_conf.ndots then
search_start = 1
search_end = #resolv_conf.search + 1
end
for i = search_start, search_end, 1 do
local new_host = resolv_conf.search[i] and string_format("%s.%s", host, resolv_conf.search[i]) or host
addresses, ttl, dns_errors = resolve_host(r, new_host)
if addresses then
cache_set(host, addresses, ttl)
return addresses
end
end
if #dns_errors > 0 then
ngx.log(ngx.ERR, "failed to query the DNS server:\n" .. table.concat(dns_errors, "\n"))
ngx_log(ngx_ERR, "failed to query the DNS server:\n" .. table_concat(dns_errors, "\n"))
end
return { host }
end
if _TEST then
_M._cache = cache
end
return _M

View file

@ -0,0 +1,79 @@
local ngx_re_split = require("ngx.re").split
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local CONF_PATH = "/etc/resolv.conf"
local nameservers, search, ndots = {}, {}, 1
local function set_search(parts)
local length = #parts
for i = 2, length, 1 do
search[i-1] = parts[i]
end
end
local function set_ndots(parts)
local option = parts[2]
if not option then
return
end
local option_parts, err = ngx_re_split(option, ":")
if err then
ngx_log(ngx_ERR, err)
return
end
if option_parts[1] ~= "ndots" then
return
end
ndots = tonumber(option_parts[2])
end
local function is_comment(line)
return line:sub(1, 1) == "#"
end
local function parse_line(line)
if is_comment(line) then
return
end
local parts, err = ngx_re_split(line, "\\s+")
if err then
ngx_log(ngx_ERR, err)
end
local keyword, value = parts[1], parts[2]
if keyword == "nameserver" then
nameservers[#nameservers + 1] = value
elseif keyword == "search" then
set_search(parts)
elseif keyword == "options" then
set_ndots(parts)
end
end
do
local f, err = io.open(CONF_PATH, "r")
if not f then
error("could not open " .. CONF_PATH .. ": " .. tostring(err))
end
for line in f:lines() do
parse_line(line)
end
f:close()
end
return {
nameservers = nameservers,
search = search,
ndots = ndots,
}

View file

@ -51,7 +51,7 @@ http {
lua_package_path "/usr/local/openresty/site/lualib/?.ljbc;/usr/local/openresty/site/lualib/?/init.ljbc;/usr/local/openresty/lualib/?.ljbc;/usr/local/openresty/lualib/?/init.ljbc;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/?/init.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua;/usr/local/lib/lua/?.lua;;";
lua_package_cpath "/usr/local/openresty/site/lualib/?.so;/usr/local/openresty/lualib/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;;";
{{ buildLuaSharedDictionaries $servers $all.Cfg.DisableLuaRestyWAF }}
{{ buildLuaSharedDictionaries $cfg $servers $all.Cfg.DisableLuaRestyWAF }}
init_by_lua_block {
collectgarbage("collect")
@ -77,7 +77,6 @@ http {
error("require failed: " .. tostring(res))
else
configuration = res
configuration.nameservers = { {{ buildResolversForLua $cfg.Resolver $cfg.DisableIpv6DNS }} }
end
ok, res = pcall(require, "balancer")
@ -96,14 +95,12 @@ http {
end
{{ end }}
{{ if $all.EnableDynamicCertificates }}
ok, res = pcall(require, "certificate")
if not ok then
error("require failed: " .. tostring(res))
else
certificate = res
end
{{ end }}
ok, res = pcall(require, "plugins")
if not ok then
@ -172,6 +169,7 @@ http {
$geoip2_dma_code source=$the_real_ip location metro_code;
$geoip2_latitude source=$the_real_ip location latitude;
$geoip2_longitude source=$the_real_ip location longitude;
$geoip2_time_zone source=$the_real_ip location time_zone;
$geoip2_region_code source=$the_real_ip subdivisions 0 iso_code;
$geoip2_region_name source=$the_real_ip subdivisions 0 names en;
}
@ -253,7 +251,7 @@ http {
# Custom headers for response
{{ range $k, $v := $addHeaders }}
add_header {{ $k }} "{{ $v }}";
add_header {{ $k }} {{ $v | quote }};
{{ end }}
server_tokens {{ if $cfg.ShowServerTokens }}on{{ else }}off{{ end }};
@ -353,6 +351,8 @@ http {
ssl_protocols {{ $cfg.SSLProtocols }};
ssl_early_data {{ if $cfg.SSLEarlyData }}on{{ else }}off{{ end }};
# turn on session caching to drastically improve performance
{{ if $cfg.SSLSessionCache }}
ssl_session_cache builtin:1000 shared:SSL:{{ $cfg.SSLSessionCacheSize }};
@ -380,12 +380,12 @@ http {
ssl_dhparam {{ $cfg.SSLDHParam }};
{{ end }}
{{ if not $cfg.EnableDynamicTLSRecords }}
ssl_dyn_rec_size_lo 0;
{{ end }}
ssl_ecdh_curve {{ $cfg.SSLECDHCurve }};
# PEM sha: {{ $cfg.DefaultSSLCertificate.PemSHA }}
ssl_certificate {{ $cfg.DefaultSSLCertificate.PemFileName }};
ssl_certificate_key {{ $cfg.DefaultSSLCertificate.PemFileName }};
{{ if gt (len $cfg.CustomHTTPErrors) 0 }}
proxy_intercept_errors on;
{{ end }}
@ -472,36 +472,14 @@ http {
{{ range $redirect := .RedirectServers }}
## start server {{ $redirect.From }}
server {
{{ range $address := $all.Cfg.BindAddressIpv4 }}
listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }} ssl;
{{ else }}
listen {{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen {{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }} ssl;
{{ end }}
{{ if $IsIPV6Enabled }}
{{ range $address := $all.Cfg.BindAddressIpv6 }}
listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }};
{{ else }}
listen [::]:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen [::]:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }};
{{ end }}
{{ end }}
server_name {{ $redirect.From }};
{{ if not (empty $redirect.SSLCert.PemFileName) }}
{{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}}
# PEM sha: {{ $redirect.SSLCert.PemSHA }}
ssl_certificate {{ $redirect.SSLCert.PemFileName }};
ssl_certificate_key {{ $redirect.SSLCert.PemFileName }};
{{ buildHTTPListener $all $redirect.From }}
{{ buildHTTPSListener $all $redirect.From }}
{{ if $all.EnableDynamicCertificates}}
ssl_certificate_by_lua_block {
certificate.call()
}
{{ end }}
{{ end }}
{{ if gt (len $cfg.BlockUserAgents) 0 }}
if ($block_ua) {
@ -604,9 +582,8 @@ http {
}
location /configuration {
# this should be equals to configuration_data dict
client_max_body_size 10m;
client_body_buffer_size 10m;
client_max_body_size {{ luaConfigurationRequestBodySize $cfg }}m;
client_body_buffer_size {{ luaConfigurationRequestBodySize $cfg }}m;
proxy_buffering off;
content_by_lua_block {
@ -639,7 +616,6 @@ stream {
error("require failed: " .. tostring(res))
else
configuration = res
configuration.nameservers = { {{ buildResolversForLua $cfg.Resolver $cfg.DisableIpv6DNS }} }
end
ok, res = pcall(require, "tcp_udp_configuration")
@ -685,7 +661,7 @@ stream {
listen unix:{{ .StreamSocket }};
access_log off;
content_by_lua_block {
tcp_udp_configuration.call()
}
@ -764,7 +740,7 @@ stream {
proxy_set_header X-Request-ID $req_id;
proxy_set_header Host $best_http_host;
set $proxy_upstream_name {{ $upstreamName }};
set $proxy_upstream_name {{ $upstreamName | quote }};
rewrite (.*) / break;
@ -804,50 +780,15 @@ stream {
{{ define "SERVER" }}
{{ $all := .First }}
{{ $server := .Second }}
{{ range $address := $all.Cfg.BindAddressIpv4 }}
listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}};
{{ else }}
listen {{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}};
{{ end }}
{{ if $all.IsIPV6Enabled }}
{{ range $address := $all.Cfg.BindAddressIpv6 }}
listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{ end }};
{{ else }}
listen [::]:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{ end }};
{{ end }}
{{ end }}
{{ buildHTTPListener $all $server.Hostname }}
{{ buildHTTPSListener $all $server.Hostname }}
set $proxy_upstream_name "-";
set $pass_access_scheme $scheme;
set $pass_server_port $server_port;
set $best_http_host $http_host;
set $pass_port $pass_server_port;
{{/* Listen on {{ $all.ListenPorts.SSLProxy }} because port {{ $all.ListenPorts.HTTPS }} is used in the TLS sni server */}}
{{/* This listener must always have proxy_protocol enabled, because the SNI listener forwards on source IP info in it. */}}
{{ if not (empty $server.SSLCert.PemFileName) }}
{{ range $address := $all.Cfg.BindAddressIpv4 }}
listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol {{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
{{ else }}
listen {{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol {{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
{{ end }}
{{ if $all.IsIPV6Enabled }}
{{ range $address := $all.Cfg.BindAddressIpv6 }}
{{ if not (empty $server.SSLCert.PemFileName) }}listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
{{ else }}
{{ if not (empty $server.SSLCert.PemFileName) }}listen [::]:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
{{ end }}
{{ end }}
{{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}}
# PEM sha: {{ $server.SSLCert.PemSHA }}
ssl_certificate {{ $server.SSLCert.PemFileName }};
ssl_certificate_key {{ $server.SSLCert.PemFileName }};
{{ if $all.EnableDynamicCertificates}}
ssl_certificate_by_lua_block {
certificate.call()
}
{{ end }}
{{ end }}
{{ if not (empty $server.AuthTLSError) }}
# {{ $server.AuthTLSError }}
@ -855,7 +796,7 @@ stream {
{{ else }}
{{ if not (empty $server.CertificateAuth.CAFileName) }}
# PEM sha: {{ $server.CertificateAuth.PemSHA }}
# PEM sha: {{ $server.CertificateAuth.CASHA }}
ssl_client_certificate {{ $server.CertificateAuth.CAFileName }};
ssl_verify_client {{ $server.CertificateAuth.VerifyClient }};
ssl_verify_depth {{ $server.CertificateAuth.ValidationDepth }};
@ -930,7 +871,7 @@ stream {
# ngx_auth_request module overrides variables in the parent request,
# therefore we have to explicitly set this variable again so that when the parent request
# resumes it has the correct value set for this variable so that Lua can pick backend correctly
set $proxy_upstream_name "{{ buildUpstreamName $location }}";
set $proxy_upstream_name {{ buildUpstreamName $location | quote }};
proxy_pass_request_body off;
proxy_set_header Content-Length "";
@ -1000,16 +941,21 @@ stream {
location {{ $path }} {
{{ $ing := (getIngressInformation $location.Ingress $server.Hostname $location.Path) }}
set $namespace "{{ $ing.Namespace }}";
set $ingress_name "{{ $ing.Rule }}";
set $service_name "{{ $ing.Service }}";
set $service_port "{{ $location.Port }}";
set $location_path "{{ $location.Path | escapeLiteralDollar }}";
set $namespace {{ $ing.Namespace | quote}};
set $ingress_name {{ $ing.Rule | quote }};
set $service_name {{ $ing.Service | quote }};
set $service_port {{ $location.Port | quote }};
set $location_path {{ $location.Path | escapeLiteralDollar | quote }};
{{ if $all.Cfg.EnableOpentracing }}
{{ opentracingPropagateContext $location }};
{{ end }}
{{ if $location.Mirror.URI }}
mirror {{ $location.Mirror.URI }};
mirror_request_body {{ $location.Mirror.RequestBody }};
{{ end }}
rewrite_by_lua_block {
lua_ingress.rewrite({{ locationConfigForLua $location $server $all }})
balancer.rewrite()
@ -1025,7 +971,7 @@ stream {
local lua_resty_waf = require("resty.waf")
local waf = lua_resty_waf:new()
waf:set_option("mode", "{{ $location.LuaRestyWAF.Mode }}")
waf:set_option("mode", {{ $location.LuaRestyWAF.Mode | quote }})
waf:set_option("storage_zone", "waf_storage")
{{ if $location.LuaRestyWAF.AllowUnknownContentTypes }}
@ -1054,7 +1000,7 @@ stream {
{{ end }}
{{ range $ruleset := $location.LuaRestyWAF.IgnoredRuleSets }}
waf:set_option("ignore_ruleset", "{{ $ruleset }}")
waf:set_option("ignore_ruleset", {{ $ruleset | quote }})
{{ end }}
{{ if gt (len $location.LuaRestyWAF.ExtraRulesetString) 0 }}
@ -1096,13 +1042,12 @@ stream {
plugins.run()
}
{{ if (and (not (empty $server.SSLCert.PemFileName)) $all.Cfg.HSTS) }}
{{ if (and $server.SSLCert $all.Cfg.HSTS) }}
if ($scheme = https) {
more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }}{{ if $all.Cfg.HSTSPreload }}; preload{{ end }}";
}
{{ end }}
{{ if not $location.Logs.Access }}
access_log off;
{{ end }}
@ -1118,8 +1063,12 @@ stream {
port_in_redirect {{ if $location.UsePortInRedirects }}on{{ else }}off{{ end }};
set $balancer_ewma_score -1;
set $proxy_upstream_name "{{ buildUpstreamName $location }}";
set $proxy_host $proxy_upstream_name;
set $proxy_upstream_name {{ buildUpstreamName $location | quote }};
set $proxy_host $proxy_upstream_name;
set $pass_access_scheme $scheme;
set $pass_server_port $server_port;
set $best_http_host $http_host;
set $pass_port $pass_server_port;
set $proxy_alternative_upstream_name "";
@ -1139,7 +1088,7 @@ stream {
{{ end }}
{{ if (not (empty $location.ModSecurity.TransactionID)) }}
modsecurity_transaction_id "{{ $location.ModSecurity.TransactionID }}";
modsecurity_transaction_id {{ $location.ModSecurity.TransactionID | quote }};
{{ end }}
{{ end }}
@ -1168,10 +1117,10 @@ stream {
{{ if $location.BasicDigestAuth.Secured }}
{{ if eq $location.BasicDigestAuth.Type "basic" }}
auth_basic "{{ $location.BasicDigestAuth.Realm }}";
auth_basic {{ $location.BasicDigestAuth.Realm | quote }};
auth_basic_user_file {{ $location.BasicDigestAuth.File }};
{{ else }}
auth_digest "{{ $location.BasicDigestAuth.Realm }}";
auth_digest {{ $location.BasicDigestAuth.Realm | quote }};
auth_digest_user_file {{ $location.BasicDigestAuth.File }};
{{ end }}
proxy_set_header Authorization "";
@ -1205,7 +1154,7 @@ stream {
{{/* By default use vhost as Host to upstream, but allow overrides */}}
{{ if not (eq $proxySetHeader "grpc_set_header") }}
{{ if not (empty $location.UpstreamVhost) }}
{{ $proxySetHeader }} Host "{{ $location.UpstreamVhost }}";
{{ $proxySetHeader }} Host {{ $location.UpstreamVhost | quote }};
{{ else }}
{{ $proxySetHeader }} Host $best_http_host;
{{ end }}
@ -1253,7 +1202,7 @@ stream {
# Custom headers to proxied server
{{ range $k, $v := $all.ProxySetHeaders }}
{{ $proxySetHeader }} {{ $k }} "{{ $v }}";
{{ $proxySetHeader }} {{ $k }} {{ $v | quote }};
{{ end }}
proxy_connect_timeout {{ $location.Proxy.ConnectTimeout }}s;
@ -1263,6 +1212,9 @@ stream {
proxy_buffering {{ $location.Proxy.ProxyBuffering }};
proxy_buffer_size {{ $location.Proxy.BufferSize }};
proxy_buffers {{ $location.Proxy.BuffersNumber }} {{ $location.Proxy.BufferSize }};
{{ if isValidByteSize $location.Proxy.ProxyMaxTempFileSize true }}
proxy_max_temp_file_size {{ $location.Proxy.ProxyMaxTempFileSize }};
{{ end }}
proxy_request_buffering {{ $location.Proxy.RequestBuffering }};
proxy_http_version {{ $location.Proxy.ProxyHTTPVersion }};
@ -1306,6 +1258,16 @@ stream {
{{ range $errCode := $location.CustomHTTPErrors }}
error_page {{ $errCode }} = @custom_{{ $location.DefaultBackendUpstreamName }}_{{ $errCode }};{{ end }}
{{ if (eq $location.BackendProtocol "FCGI") }}
include /etc/nginx/fastcgi_params;
{{ end }}
{{- if $location.FastCGI.Index -}}
fastcgi_index {{ $location.FastCGI.Index | quote }};
{{- end -}}
{{ range $k, $v := $location.FastCGI.Params }}
fastcgi_param {{ $k }} {{ $v | quote }};
{{ end }}
{{ buildProxyPass $server.Hostname $all.Backends $location }}
{{ if (or (eq $location.Proxy.ProxyRedirectFrom "default") (eq $location.Proxy.ProxyRedirectFrom "off")) }}
proxy_redirect {{ $location.Proxy.ProxyRedirectFrom }};
@ -1313,7 +1275,7 @@ stream {
proxy_redirect {{ $location.Proxy.ProxyRedirectFrom }} {{ $location.Proxy.ProxyRedirectTo }};
{{ end }}
{{ else }}
# Location denied. Reason: {{ $location.Denied | printf "%q" }}
# Location denied. Reason: {{ $location.Denied | quote }}
return 503;
{{ end }}
}