From 127ee2d5c945b6af7e0a7df9a6390d01a624f54a Mon Sep 17 00:00:00 2001 From: Aapo Talvensaari Date: Wed, 6 May 2020 20:39:15 +0300 Subject: [PATCH] feat(conf) add port_map configuration option ### 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`.` ``` 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. --- kong.conf.default | 7 +++++++ kong/conf_loader.lua | 29 ++++++++++++++++++++++++++ kong/pdk/request.lua | 9 +++++++- kong/plugins/log-serializers/basic.lua | 4 +++- kong/router.lua | 3 ++- kong/runloop/handler.lua | 14 ++++++++++++- kong/templates/kong_defaults.lua | 2 ++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/kong.conf.default b/kong.conf.default index 2b4ac168cdf9..a06ee3695ca0 100644 --- a/kong.conf.default +++ b/kong.conf.default @@ -29,6 +29,13 @@ # 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`.` + #log_level = notice # Log level of the Nginx server. Logs are # found at `/logs/error.log`. diff --git a/kong/conf_loader.lua b/kong/conf_loader.lua index 93c06d9bebbe..d0e483440365 100644 --- a/kong/conf_loader.lua +++ b/kong/conf_loader.lua @@ -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" }, @@ -571,6 +572,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_str + 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 diff --git a/kong/pdk/request.lua b/kong/pdk/request.lua index a24e1dbf3f57..73366e065fc0 100644 --- a/kong/pdk/request.lua +++ b/kong/pdk/request.lua @@ -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 @@ -232,7 +233,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 diff --git a/kong/plugins/log-serializers/basic.lua b/kong/plugins/log-serializers/basic.lua index a701be0003fa..94a62df91cf8 100644 --- a/kong/plugins/log-serializers/basic.lua +++ b/kong/plugins/log-serializers/basic.lua @@ -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, diff --git a/kong/router.lua b/kong/router.lua index aba2e3c4c8b1..1165b1c69c4c 100644 --- a/kong/router.lua +++ b/kong/router.lua @@ -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(ctx.host_port, 10) + or tonumber(var.server_port, 10) -- error value for non-TLS connections ignored intentionally local sni, _ = server_name() diff --git a/kong/runloop/handler.lua b/kong/runloop/handler.lua index e23e4e465ebe..d19db80da3e2 100644 --- a/kong/runloop/handler.lua +++ b/kong/runloop/handler.lua @@ -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 } @@ -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) @@ -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() @@ -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 @@ -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 diff --git a/kong/templates/kong_defaults.lua b/kong/templates/kong_defaults.lua index c611176eab23..6e1b6764b8d4 100644 --- a/kong/templates/kong_defaults.lua +++ b/kong/templates/kong_defaults.lua @@ -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