Skip to content

Commit

Permalink
feat(conf) add port_map configuration option
Browse files Browse the repository at this point in the history
### Summary

Layer 4 port mapping (or port forwarding) is used a lot when running
Kong inside a container. Common example is exposing ports 80 and 443
externally while running Kong inside a container with internal ports
8000 and 8443. It also allows Kong to started without elevated
privileges as it is quite common that you need root-access to bind
processes to TCP port < 1024.

As this translation is done in many cases without using layer 4
proxy_protocol or layer 7 X-Forwarded-Port HTTP header, the Kong has
no knowledge about the target port that client originally connected
to.

This PR adds `port_map` configuration parameter:

```
#port_map =                      # When running Kong behind layer 4 port mapping
                                 # (or port-forwarding), e.g. with
                                 # `docker run -p 80:8000`, you can use this
                                 # parameter to let Kong know about published
                                 # port when it differs from the one Kong is
                                 # listening, e.g. port_map=80:8000, 443:8443`.`
                                 # It has effect on `X-Forwarded-Port`, stream
                                 # routing by destination port, PDK function
                                 # `kong.request.get_forwarded_port()`, and
                                 # on basic log serializer.
```

This gets parsed into `kong.configuration.host_ports` table.

E.g. `KONG_PORT_MAP="80:8000,443:8443"` where the first number (`80`/`443`) before `:`
is the host published port and the second one (`8000`/`8443`) is the port that Kong is
listening translates to following `kong.configuration.host_ports`:

```lua
{
  [8000]   = 80,
  ["8000"] = 80,
  [8443]   = 443,
  ["8443"] = 443,
}
```

This PR also adds a new nginx context variable `ngx.ctx.host_port` which
contains this translated port. So instead of using `ngx.var.server_port`
the more correct one in many cases is `ngx.ctx.host_port`.

Kong PDK is also changed to take this in account when using:
```
local port = kong.request.get_forwarded_port()
```

This commit also changes the `X-Forwarded-Port` to use this. Additionally
`stream routing` by `destination port` will use this too. Basic log
serializer was changed to use `ngx.var.host_port` as well.

Currently it is only usable in proxy/stream, so things like `admin api`
is not affected by this.
  • Loading branch information
bungle committed May 12, 2020
1 parent eeab2ec commit a6a3961
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 34 deletions.
11 changes: 11 additions & 0 deletions kong.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
# Each Kong process must have a separate
# working directory.

#port_map = # When running Kong behind layer 4 port mapping
# (or port-forwarding), e.g. with
# `docker run -p 80:8000`, you can use this
# parameter to let Kong know about published
# port when it differs from the one Kong is
# listening, e.g. port_map=80:8000, 443:8443`.`
# It has effect on `X-Forwarded-Port`, stream
# routing by destination port, PDK function
# `kong.request.get_forwarded_port()`, and
# on basic log serializer.

#log_level = notice # Log level of the Nginx server. Logs are
# found at `<prefix>/logs/error.log`.

Expand Down
29 changes: 29 additions & 0 deletions kong/conf_loader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ local PREFIX_PATHS = {
-- `array`: a comma-separated list
local CONF_INFERENCES = {
-- forced string inferences (or else are retrieved as numbers)
port_map = { typ = "array" },
proxy_listen = { typ = "array" },
admin_listen = { typ = "array" },
status_listen = { typ = "array" },
Expand Down Expand Up @@ -597,6 +598,34 @@ local function check_and_infer(conf, opts)
-- custom validations
---------------------

conf.host_ports = {}
if conf.port_map then
local MIN_PORT = 1
local MAX_PORT = 65535

for _, ports in ipairs(conf.port_map) do
local colpos = string.find(ports, ":", nil, true)
if not colpos then
errors[#errors + 1] = "invalid port mapping (`port_map`): " .. ports

else
local host_port_str = string.sub(ports, 1, colpos - 1)
local host_port_num = tonumber(host_port_str, 10)
local kong_port_str = string.sub(ports, colpos + 1)
local kong_port_num = tonumber(kong_port_str, 10)

if (host_port_num and host_port_num >= MIN_PORT and host_port_num <= MAX_PORT)
and (kong_port_num and kong_port_num >= MIN_PORT and kong_port_num <= MAX_PORT)
then
conf.host_ports[kong_port_num] = host_port_num
conf.host_ports[kong_port_str] = host_port_num
else
errors[#errors + 1] = "invalid port mapping (`port_map`): " .. ports
end
end
end
end

if conf.database == "cassandra" then
if string.find(conf.cassandra_lb_policy, "DCAware", nil, true)
and not conf.cassandra_local_datacenter
Expand Down
16 changes: 15 additions & 1 deletion kong/pdk/request.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ cjson.decode_array_with_array_mt(true)
local function new(self)
local _REQUEST = {}

local HOST_PORTS = self.configuration.host_ports or {}

local MIN_HEADERS = 1
local MAX_HEADERS_DEFAULT = 100
Expand Down Expand Up @@ -200,6 +201,13 @@ local function new(self)
-- **Note**: we do not currently offer support for Forwarded HTTP Extension
-- (RFC 7239) since it is not supported by ngx_http_realip_module.
--
-- When running Kong behind the L4 port mapping (or forwarding) you can also
-- configure:
-- * [port\_map](https://getkong.org/docs/latest/configuration/#port_map)
--
-- `port_map` configuration parameter enables this function to return the published
-- port instead of the port that Kong is listening to (in case they differ).
--
-- @function kong.request.get_forwarded_port
-- @phases rewrite, access, header_filter, body_filter, log, admin_api
-- @treturn number the forwarded port
Expand Down Expand Up @@ -232,7 +240,13 @@ local function new(self)
end
end

return _REQUEST.get_port()
local host_port = ngx.ctx.host_port
if host_port then
return host_port
end

local port = _REQUEST.get_port()
return HOST_PORTS[port] or port
end


Expand Down
4 changes: 3 additions & 1 deletion kong/plugins/log-serializers/basic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ function _M.serialize(ngx, kong)
end
end

local host_port = ctx.host_port or var.server_port

return {
request = {
uri = request_uri,
url = var.scheme .. "://" .. var.host .. ":" .. var.server_port .. request_uri,
url = var.scheme .. "://" .. var.host .. ":" .. host_port .. request_uri,
querystring = kong.request.get_query(), -- parameters, as a table
method = kong.request.get_method(), -- http method
headers = req_headers,
Expand Down
3 changes: 2 additions & 1 deletion kong/router.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1755,7 +1755,8 @@ function _M.new(routes)
local src_ip = var.remote_addr
local src_port = tonumber(var.remote_port, 10)
local dst_ip = var.server_addr
local dst_port = tonumber(var.server_port, 10)
local dst_port = tonumber(ngx.ctx.host_port, 10)
or tonumber(var.server_port, 10)
-- error value for non-TLS connections ignored intentionally
local sni, _ = server_name()

Expand Down
14 changes: 13 additions & 1 deletion kong/runloop/handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ local COMMA = byte(",")
local SPACE = byte(" ")


local HOST_PORTS = {}


local SUBSYSTEMS = constants.PROTOCOLS_WITH_SUBSYSTEM
local EMPTY_T = {}
local TTL_ZERO = { ttl = 0 }
Expand Down Expand Up @@ -871,6 +874,10 @@ return {

init_worker = {
before = function()
if kong.configuration.host_ports then
HOST_PORTS = kong.configuration.host_ports
end

if kong.configuration.anonymous_reports then
reports.configure_ping(kong.configuration)
reports.add_ping_value("database_version", kong.db.infos.db_ver)
Expand Down Expand Up @@ -953,6 +960,8 @@ return {
},
preread = {
before = function(ctx)
ctx.host_port = HOST_PORTS[var.server_port] or var.server_port

local router = get_updated_router()

local match_t = router.exec()
Expand Down Expand Up @@ -986,6 +995,8 @@ return {
},
rewrite = {
before = function(ctx)
ctx.host_port = HOST_PORTS[var.server_port] or var.server_port

-- special handling for proxy-authorization and te headers in case
-- the plugin(s) want to specify them (store the original)
ctx.http_proxy_authorization = var.http_proxy_authorization
Expand All @@ -1012,7 +1023,8 @@ return {
local http_version = ngx.req.http_version()
local scheme = var.scheme
local host = var.host
local port = tonumber(var.server_port, 10)
local port = tonumber(ctx.host_port, 10)
or tonumber(var.server_port, 10)
local content_type = var.content_type

local route = match_t.route
Expand Down
2 changes: 2 additions & 0 deletions kong/templates/kong_defaults.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
return [[
prefix = /usr/local/kong/
port_map = NONE
host_ports = NONE
log_level = notice
proxy_access_log = logs/access.log
proxy_error_log = logs/error.log
Expand Down
61 changes: 45 additions & 16 deletions spec/01-unit/03-conf_loader_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe("Configuration loader", function()
assert.is_nil(err)

assert.True(search_directive(conf.nginx_http_directives,
"max_pending_timers", "4096"))
"max_pending_timers", "4096"))
end)

it("accepts flexible config values with precedence", function()
Expand Down Expand Up @@ -371,25 +371,25 @@ describe("Configuration loader", function()
it("is injected if not provided via nginx_http_* directives", function()
local conf = assert(conf_loader())
assert.True(search_directive(conf.nginx_http_directives,
"lua_shared_dict", "prometheus_metrics 5m"))
"lua_shared_dict", "prometheus_metrics 5m"))
end)
it("size is not modified if provided via nginx_http_* directives", function()
local conf = assert(conf_loader(nil, {
plugins = "bundled",
nginx_http_lua_shared_dict = "prometheus_metrics 2m",
}))
assert.True(search_directive(conf.nginx_http_directives,
"lua_shared_dict", "prometheus_metrics 2m"))
"lua_shared_dict", "prometheus_metrics 2m"))
end)
it("is injected in addition to any shm provided via nginx_http_* directive", function()
local conf = assert(conf_loader(nil, {
plugins = "bundled",
nginx_http_lua_shared_dict = "custom_cache 2m",
}))
assert.True(search_directive(conf.nginx_http_directives,
"lua_shared_dict", "custom_cache 2m"))
"lua_shared_dict", "custom_cache 2m"))
assert.True(search_directive(conf.nginx_http_directives,
"lua_shared_dict", "prometheus_metrics 5m"))
"lua_shared_dict", "prometheus_metrics 5m"))
end)
it("is not injected if prometheus plugin is disabled", function()
local conf = assert(conf_loader(nil, {
Expand Down Expand Up @@ -445,6 +445,35 @@ describe("Configuration loader", function()
end)
end)

describe("port_map and host_ports", function()
it("are empty tables when not specified", function()
local conf = assert(conf_loader(helpers.test_conf_path, {}))
assert.same({}, conf.port_map)
assert.same({}, conf.host_ports)
end)
it("are tables when specified", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
port_map = "80:8000,443:8443",
}))
assert.same({
"80:8000",
"443:8443",
}, conf.port_map)
assert.same({
[8000] = 80,
["8000"] = 80,
[8443] = 443,
["8443"] = 443,
}, conf.host_ports)
end)
it("gives an error with invalid value", function()
local _, err = conf_loader(helpers.test_conf_path, {
port_map = "src:dst",
})
assert.equal("invalid port mapping (`port_map`): src:dst", err)
end)
end)

describe("inferences", function()
it("infer booleans (on/off/true/false strings)", function()
local conf = assert(conf_loader())
Expand Down Expand Up @@ -531,16 +560,16 @@ describe("Configuration loader", function()
cassandra_write_consistency = "FOUR"
})
assert.equal("cassandra_write_consistency has an invalid value: 'FOUR'" ..
" (ALL, EACH_QUORUM, QUORUM, LOCAL_QUORUM, ONE, TWO," ..
" THREE, LOCAL_ONE)", err)
" (ALL, EACH_QUORUM, QUORUM, LOCAL_QUORUM, ONE, TWO," ..
" THREE, LOCAL_ONE)", err)
assert.is_nil(conf)

conf, err = conf_loader(nil, {
cassandra_read_consistency = "FOUR"
})
assert.equal("cassandra_read_consistency has an invalid value: 'FOUR'" ..
" (ALL, EACH_QUORUM, QUORUM, LOCAL_QUORUM, ONE, TWO," ..
" THREE, LOCAL_ONE)", err)
" (ALL, EACH_QUORUM, QUORUM, LOCAL_QUORUM, ONE, TWO," ..
" THREE, LOCAL_ONE)", err)
assert.is_nil(conf)
end)
it("enforces listen addresses format", function()
Expand Down Expand Up @@ -627,24 +656,24 @@ describe("Configuration loader", function()
end)
it("errors when hosts have a bad format in cassandra_contact_points", function()
local conf, err = conf_loader(nil, {
database = "cassandra",
cassandra_contact_points = [[some/really\bad/host\name,addr2]]
database = "cassandra",
cassandra_contact_points = [[some/really\bad/host\name,addr2]]
})
assert.equal([[bad cassandra contact point 'some/really\bad/host\name': invalid hostname: some/really\bad/host\name]], err)
assert.is_nil(conf)
end)
it("errors cassandra_refresh_frequency is < 0", function()
local conf, err = conf_loader(nil, {
database = "cassandra",
cassandra_refresh_frequency = -1,
database = "cassandra",
cassandra_refresh_frequency = -1,
})
assert.equal("cassandra_refresh_frequency must be 0 or greater", err)
assert.is_nil(conf)
end)
it("errors when specifying a port in cassandra_contact_points", function()
local conf, err = conf_loader(nil, {
database = "cassandra",
cassandra_contact_points = "addr1:9042,addr2"
database = "cassandra",
cassandra_contact_points = "addr1:9042,addr2"
})
assert.equal("bad cassandra contact point 'addr1:9042': port must be specified in cassandra_port", err)
assert.is_nil(conf)
Expand Down Expand Up @@ -903,7 +932,7 @@ describe("Configuration loader", function()
})
assert.is_nil(conf)
assert.equal("must specify 'cassandra_local_datacenter' when " ..
policy .. " policy is in use", err)
policy .. " policy is in use", err)
end
end)
it("honors path if provided even if a default file exists", function()
Expand Down
36 changes: 22 additions & 14 deletions spec/01-unit/10-log_serializer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ describe("Log Serializer", function()
assert.is_table(res.tries)
end)

it("uses port map (ngx.ctx.host_port) for request url ", function()
ngx.ctx.host_port = 5000
local res = basic.serialize(ngx, kong)
assert.is_table(res)
assert.is_table(res.request)
assert.equal("http://test.com:5000/request_uri", res.request.url)
end)

it("serializes the matching Route and Services", function()
ngx.ctx.route = { id = "my_route" }
ngx.ctx.service = { id = "my_service" }
Expand Down Expand Up @@ -127,20 +135,20 @@ describe("Log Serializer", function()
assert.is_table(res)

assert.same({
{
code = 502,
ip = '127.0.0.1',
port = 1234,
state = 'next',
}, {
ip = '127.0.0.1',
port = 1234,
state = 'failed',
}, {
ip = '127.0.0.1',
port = 1234,
},
}, res.tries)
{
code = 502,
ip = '127.0.0.1',
port = 1234,
state = 'next',
}, {
ip = '127.0.0.1',
port = 1234,
state = 'failed',
}, {
ip = '127.0.0.1',
port = 1234,
},
}, res.tries)
end)

it("does not fail when the 'balancer_data' structure is missing", function()
Expand Down
Loading

0 comments on commit a6a3961

Please sign in to comment.