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_maps` configuration parameter:

```
#port_maps =                     # With this configuration parameter you can
                                 # let the Kong to know about the port from
                                 # which the packets are forwarded to it. This
                                 # is fairly common when running Kong in a
                                 # containerized or virtualized environment.
                                 # For example `port_maps=80:8000, 443:8443`
                                 # instructs Kong that the port 80 is mapped
                                 # to 8000 (and the port 443 to 8443), where
                                 # 8000 and 8443 are the ports the Kong is
                                 # listening to.
                                 #
                                 # This parameter helps Kong to set proper
                                 # forwarded upstream HTTP request header or to
                                 # get proper forwarded port with a Kong PDK
                                 # (in case other means determining it have
                                 # failed). It changes routing by a destination
                                 # port to route by a port from which packets
                                 # are forwarded to Kong, and similarly it
                                 # changes the default plugin log serializer to
                                 # use the port according to this mapping
                                 # instead of reporting the port Kong is
                                 # listening to.
```

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 29, 2020
1 parent 7346142 commit 6a7a277
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 4 deletions.
23 changes: 23 additions & 0 deletions kong.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@
# This value can be set to `off`, thus disabling
# the plugin server and Go plugin loading.

#port_maps = # With this configuration parameter you can
# let the Kong to know about the port from
# which the packets are forwarded to it. This
# is fairly common when running Kong in a
# containerized or virtualized environment.
# For example `port_maps=80:8000, 443:8443`
# instructs Kong that the port 80 is mapped
# to 8000 (and the port 443 to 8443), where
# 8000 and 8443 are the ports the Kong is
# listening to.
#
# This parameter helps Kong to set proper
# forwarded upstream HTTP request header or to
# get proper forwarded port with a Kong PDK
# (in case other means determining it have
# failed). It changes routing by a destination
# port to route by a port from which packets
# are forwarded to Kong, and similarly it
# changes the default plugin log serializer to
# use the port according to this mapping
# instead of reporting the port Kong is
# listening to.

#anonymous_reports = on # Send anonymous usage data such as error
# stack traces to help improve Kong.

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_maps = { 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_maps then
local MIN_PORT = 1
local MAX_PORT = 65535

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

else
local host_port_str = string.sub(port_map, 1, colpos - 1)
local host_port_num = tonumber(host_port_str, 10)
local kong_port_str = string.sub(port_map, 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_maps`): " .. port_map
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\_maps](https://getkong.org/docs/latest/configuration/#port_maps)
--
-- `port_maps` configuration parameter enables this function to return the
-- port to which the port Kong is listening to is mapped 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
Expand Up @@ -10,6 +10,8 @@ status_error_log = logs/status_error.log
plugins = bundled
go_pluginserver_exe = /usr/local/bin/go-pluginserver
go_plugins_dir = off
port_maps = NONE
host_ports = NONE
anonymous_reports = on
proxy_listen = 0.0.0.0:8000 reuseport backlog=16384, 0.0.0.0:8443 http2 ssl reuseport backlog=16384
Expand Down
29 changes: 29 additions & 0 deletions spec/01-unit/03-conf_loader_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,35 @@ describe("Configuration loader", function()
end)
end)

describe("port_maps 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_maps)
assert.same({}, conf.host_ports)
end)
it("are tables when specified", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
port_maps = "80:8000,443:8443",
}))
assert.same({
"80:8000",
"443:8443",
}, conf.port_maps)
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_maps = "src:dst",
})
assert.equal("invalid port mapping (`port_maps`): src:dst", err)
end)
end)

describe("inferences", function()
it("infer booleans (on/off/true/false strings)", function()
local conf = assert(conf_loader())
Expand Down
8 changes: 8 additions & 0 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
78 changes: 78 additions & 0 deletions spec/02-integration/05-proxy/01-proxy_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,81 @@ describe("#stream proxy interface listeners", function()
end
end)
end)

for _, strategy in helpers.each_strategy() do
if strategy ~= "off" then
describe("[stream]", function()
local MESSAGE = "echo, ping, pong. echo, ping, pong. echo, ping, pong.\n"
lazy_setup(function()
local bp = helpers.get_db_utils(strategy, {
"routes",
"services",
"plugins",
})

local service = assert(bp.services:insert {
host = helpers.mock_upstream_host,
port = helpers.mock_upstream_stream_port,
protocol = "tcp",
})

assert(bp.routes:insert {
destinations = {
{ port = 19000 },
},
protocols = {
"tcp",
},
service = service,
})

assert(helpers.start_kong({
database = strategy,
stream_listen = helpers.get_proxy_ip(false) .. ":19000, " ..
helpers.get_proxy_ip(false) .. ":18000, " ..
helpers.get_proxy_ip(false) .. ":17000",
port_maps = "19000:18000",
plugins = "bundled,ctx-tests",
nginx_conf = "spec/fixtures/custom_nginx.template",
proxy_listen = "off",
admin_listen = "off",
}))
end)

lazy_teardown(function()
helpers.stop_kong()
end)

it("routes by destination port without port map", function()
local tcp_client = ngx.socket.tcp()
assert(tcp_client:connect(helpers.get_proxy_ip(false), 19000))
assert(tcp_client:send(MESSAGE))
local body = assert(tcp_client:receive("*a"))
assert.equal(MESSAGE, body)
assert(tcp_client:close())
end)

it("uses port maps configuration to route by destination port", function()
local tcp_client = ngx.socket.tcp()
assert(tcp_client:connect(helpers.get_proxy_ip(false), 18000))
assert(tcp_client:send(MESSAGE))
local body = assert(tcp_client:receive("*a"))
assert.equal(MESSAGE, body)
assert(tcp_client:close())
end)

it("fails to route when no port map is specified and route is not found", function()
local tcp_client = ngx.socket.tcp()
assert(tcp_client:connect(helpers.get_proxy_ip(false), 17000))
assert(tcp_client:send(MESSAGE))
local body, err = tcp_client:receive("*a")
if not err then
assert.equal("", body)
else
assert.equal("connection reset by peer", err)
end
assert(tcp_client:close())
end)
end)
end
end
32 changes: 32 additions & 0 deletions spec/02-integration/05-proxy/03-upstream_headers_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -970,5 +970,37 @@ for _, strategy in helpers.each_strategy() do
end)
end)
end)

describe("(using port maps configuration)", function()
local proxy_port = helpers.get_proxy_port(false)

lazy_setup(start_kong {
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
lua_package_path = "?/init.lua;./kong/?.lua;./spec/fixtures/?.lua",
port_maps = "80:" .. proxy_port,
})

lazy_teardown(stop_kong)

describe("X-Forwarded-Port", function()
it("should be added if not present in request", function()
local headers = request_headers {
["Host"] = "headers-inspect.com",
}

assert.equal(80, tonumber(headers["x-forwarded-port"]))
end)

it("should be replaced if present in request", function()
local headers = request_headers {
["Host"] = "headers-inspect.com",
["X-Forwarded-Port"] = "81",
}

assert.equal(80, tonumber(headers["x-forwarded-port"]))
end)
end)
end)
end)
end
Loading

0 comments on commit 6a7a277

Please sign in to comment.