Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy Protocol support #1211

Merged
merged 6 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- New content_caching Prometheus metric [THREESCALE-5439](https://issues.jboss.org/browse/THREESCALE-5439) [PR #1203](https://github.com/3scale/APIcast/pull/1203)

- Added Camel policy [PR #1193](https://github.com/3scale/APIcast/pull/1193) [THREESCALE-4867](https://issues.jboss.org/browse/THREESCALE-4867)
- Support Proxy Protocol [PR #1211](https://github.com/3scale/APIcast/pull/1211) [THREESCALE-5366](https://issues.redhat.com/browse/THREESCALE-5366)

## [3.8.0] - 2020-03-24

Expand Down
16 changes: 16 additions & 0 deletions doc/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,22 @@ Defines a HTTP proxy to be used for connecting to HTTPS services. Authentication

Defines a comma-separated list of hostnames and domain names for which the requests should not be proxied. Setting to a single `*` character, which matches all hosts, effectively disables the proxy.

### `APICAST_HTTP_PROXY_PROTOCOL`

**Default:** false
**Values:** boolean
**Example:** "true"

This parameter enables the Proxy Protocol for the HTTP listener.

### `APICAST_HTTPS_PROXY_PROTOCOL`

**Default:** false
**Values:** boolean
**Example:** "true"

This parameter enables the Proxy Protocol for the HTTPS listener.

### `APICAST_EXTENDED_METRICS`

**Default:** false
Expand Down
9 changes: 6 additions & 3 deletions gateway/http.d/apicast.conf.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,17 @@ server {

{%- assign http_port = port.apicast | default: 8080 %}
{%- assign https_port = env.APICAST_HTTPS_PORT %}
{%- assign http_proxy_protocol = env.APICAST_HTTP_PROXY_PROTOCOL %}
{%- assign https_proxy_protocol = env.APICAST_HTTPS_PROXY_PROTOCOL %}

{% if http_port != https_port -%}
listen {{ http_port }};
listen {{ http_port }} {% if http_proxy_protocol %}proxy_protocol {% endif %};
{% endif %}

{% if https_port -%}
listen {{ https_port }} ssl http2;
ssl_protocols TLSv1.2 TLSv1.3;
listen {{ https_port }} ssl http2 {% if https_proxy_protocol %}proxy_protocol{% endif %};

ssl_protocols TLSv1.2 TLSv1.3;
{%- assign https_certificate = env.APICAST_HTTPS_CERTIFICATE -%}
ssl_certificate {% if https_certificate -%}
{{ https_certificate }}
Expand Down
4 changes: 4 additions & 0 deletions gateway/src/apicast/policy/ip_check/apicast-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"enum": ["X-Real-IP"],
"title": "Get the IP from the X-Real-IP header"
},
{
"enum": ["proxy_protocol_addr"],
"title": "Get the IP from the proxy_protocol_addr variable"
},
{
"enum": ["last_caller"],
"title": "Use the IP of the last caller"
Expand Down
7 changes: 6 additions & 1 deletion gateway/src/apicast/policy/ip_check/client_ip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ local function ip_from_x_forwarded_for_header()
return uri.host
end

local function ip_from_proxy_protocol_addr_variable()
return ngx.var.proxy_protocol_addr
end

local get_ip_func = {
last_caller = last_caller_ip,
["X-Real-IP"] = ip_from_x_real_ip_header,
["X-Forwarded-For"] = ip_from_x_forwarded_for_header
["X-Forwarded-For"] = ip_from_x_forwarded_for_header,
["proxy_protocol_addr"] = ip_from_proxy_protocol_addr_variable
}

function _M.get_from(sources)
Expand Down
10 changes: 10 additions & 0 deletions spec/policy/ip_check/client_ip_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ describe('ClientIP', function()
end)
end)

describe('when the source is the proxy_protocol_addr variable', function()
it('returns the IP in ngx.var.proxy_protocol_addr', function()
ngx.var = { proxy_protocol_addr = '1.2.3.4' }

local ip = client_ip.get_from({ 'proxy_protocol_addr' })

assert.equals('1.2.3.4', ip)
end)
end)

describe('when no source is given', function()
it('returns nil', function()
local ip = client_ip.get_from({})
Expand Down
2 changes: 1 addition & 1 deletion spec/policy/ip_check/ip_check_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ local IpCheckPolicy = require('apicast.policy.ip_check')
local ClientIP = require('apicast.policy.ip_check.client_ip')
local iputils = require("resty.iputils")

describe('Headers policy', function()
describe('IP Check policy', function()
before_each(function()
stub(ngx, 'say')
stub(ngx, 'exit')
Expand Down
189 changes: 189 additions & 0 deletions t/proxy-protocol.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use lib 't';
use Test::APIcast::Blackbox 'no_plan';

# These test are a bit difficult to understand. Due to the Nginx is set to listen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# These test are a bit difficult to understand. Due to the Nginx is set to listen
# Due to the Nginx is set to listen

# proxy_protocol, all backends upstream need to use another port, if not the
# backend client and API will fail, this is why a new server is added on
# --backend test section
#
# Additionally, this is the reason why the IP Check policy is tested here.


require("policies.pl");
repeat_each(3);
$ENV{TEST_NGINX_HTML_DIR} ||= "$Test::Nginx::Util::ServRoot/html";
run_tests();

__DATA__

=== TEST 1: Simple proxy-procol connection
--- init eval
$Test::Nginx::Util::BACKEND_PORT = Test::APIcast::get_random_port();
--- env eval
(
"BACKEND_ENDPOINT_OVERRIDE"=> "http://127.0.0.1:$Test::Nginx::Util::BACKEND_PORT",
"APICAST_HTTP_PROXY_PROTOCOL" => "true"
)
--- configuration eval
<<EOF
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"hosts": [
"localhost"
],
"api_backend": "http://127.0.0.1:$Test::Nginx::Util::BACKEND_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast",
"version": "builtin",
"configuration": {}
}
]
}
}
]
}
EOF
--- backend eval
<<EOF
}
server {
listen $Test::Nginx::Util::BACKEND_PORT;
server_name _ default_server;
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}

location /foo {
access_by_lua_block {
ngx.say("API BACKEND")
}
}
EOF
--- test env
content_by_lua_block {
local sock = ngx.socket.tcp()
sock:settimeout(2000)

local ok, err = sock:connect(ngx.var.server_addr, ngx.var.apicast_port)
if not ok then
ngx.say("failed to connect: ", err)
return
end

ngx.say("connected: ", ok)
sock:send(string.format("PROXY TCP4 127.0.0.1 %s %s %s\r\n\r\n\r\n", ngx.var.server_addr, "10000", ngx.var.apicast_port))
sock:send("GET /foo?user_key=123 HTTP/1.1\r\nHost: localhost\r\n\r\n")

local data = sock:receive()
ngx.say(data)
}
--- response_body env
connected: 1
HTTP/1.1 200 OK
--- error_code: 200
--- no_error_log
[error]


=== TEST 2: Simple IP check policy using proxy_protocol
--- init eval
$Test::Nginx::Util::BACKEND_PORT = Test::APIcast::get_random_port();
--- env eval
(
"BACKEND_ENDPOINT_OVERRIDE"=> "http://127.0.0.1:$Test::Nginx::Util::BACKEND_PORT",
"APICAST_HTTP_PROXY_PROTOCOL" => "true"
)
--- configuration eval
<<EOF
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"hosts": [
"localhost"
],
"api_backend": "http://127.0.0.1:$Test::Nginx::Util::BACKEND_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast.policy.ip_check",
"configuration": {
"ips": [ "9.9.9.9" ],
"client_ip_sources": [
"proxy_protocol_addr"
],
"check_type": "blacklist",
"error_msg": "A custom error message"
}
},
{
"name": "apicast",
"version": "builtin",
"configuration": {}
}
]
}
}
]
}
EOF
--- backend eval
<<EOF
}
server {
listen $Test::Nginx::Util::BACKEND_PORT;
server_name _ default_server;
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}

location /foo {
access_by_lua_block {
ngx.say("API BACKEND")
}
}
EOF
--- test env
content_by_lua_block {
local sock = ngx.socket.tcp()
sock:settimeout(2000)

local ok, err = sock:connect(ngx.var.server_addr, ngx.var.apicast_port)
if not ok then
ngx.say("failed to connect: ", err)
return
end

ngx.say("connected: ", ok)
sock:send(string.format("PROXY TCP4 9.9.9.9 %s %s %s\r\n\r\n\r\n", ngx.var.server_addr, "10000", ngx.var.apicast_port))
sock:send("GET /foo?user_key=123 HTTP/1.1\r\nHost: localhost\r\n\r\n")

local data = sock:receive()
ngx.say(data)
}
--- response_body env
connected: 1
HTTP/1.1 403 Forbidden
--- error_code: 200
--- no_error_log
[error]