diff --git a/docs/README.md b/docs/README.md index 90f5bb4..2766877 100644 --- a/docs/README.md +++ b/docs/README.md @@ -805,6 +805,25 @@ For legacy compatibility reasons, `nginx-proxy` forwards any client-supplied `X- The default for `TRUST_DOWNSTREAM_PROXY` may change to `false` in a future version of `nginx-proxy`. If you require it to be enabled, you are encouraged to explicitly set it to `true` to avoid compatibility problems when upgrading. +### Proxy Protocol Support + +`nginx-proxy` has support for the [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). This allows a separate proxy to send requests to `nginx-proxy` and encode information about the client connection without relying on HTTP headers. This can be enabled by setting `ENABLE_PROXY_PROTOCOL=true` on the main `nginx-proxy` container. It's important to note that enabling the proxy protocol will require all connections to `nginx-proxy` to use the protocol. + +You can use this feature in conjunction with the `realip` module in nginx. This will allow for setting the `$remote_addr` and `$remote_port` nginx variables to the IP and port that are provided from the protocol message. Documentation for this functionality can be found in the [nginx documentation](https://nginx.org/en/docs/http/ngx_http_realip_module.html). + +A simple example is as follows: + +1. Create a configuration file for nginx, this can be global (in `conf.d`) or host specific (in `vhost.d`) +2. Add your `realip` configuration: + +```nginx +# Your proxy server ip address +set_real_ip_from 192.168.1.0/24; + +# Where to replace `$remote_addr` and `$remote_port` from +real_ip_header proxy_protocol; +``` + ⬆️ [back to table of contents](#table-of-contents) ## Custom Nginx Configuration @@ -1329,6 +1348,7 @@ Configuration available either on the nginx-proxy container, or the docker-gen c | [`ENABLE_HTTP2`](#http2-support) | `true` | | [`ENABLE_HTTP3`](#http3-support) | `false` | | [`ENABLE_IPV6`](#listening-on-ipv6) | `false` | +| [`ENABLE_PROXY_PROTOCOL`](#proxy-protocol-support) | `false` | | [`HTTP_PORT`](#custom-external-httphttps-ports) | `80` | | [`HTTPS_PORT`](#custom-external-httphttps-ports) | `443` | | [`HTTPS_METHOD`](#how-ssl-support-works) | `redirect` | @@ -1451,6 +1471,7 @@ curl -s -H "Host: test.nginx-proxy.tld" localhost/nginx-proxy-debug | jq "enable_debug_endpoint": "true", "enable_http2": "true", "enable_http3": "false", + "enable_proxy_protocol": "false", "enable_http_on_missing_cert": "true", "enable_ipv6": false, "enable_json_logs": false, diff --git a/nginx.tmpl b/nginx.tmpl index 939aa2e..cf8ccb2 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -31,6 +31,7 @@ {{- $_ := set $config "acme_http_challenge_accept_unknown_host" ($globals.Env.ACME_HTTP_CHALLENGE_ACCEPT_UNKNOWN_HOST | default "false" | parseBool) }} {{- $_ := set $config "enable_http2" ($globals.Env.ENABLE_HTTP2 | default "true") }} {{- $_ := set $config "enable_http3" ($globals.Env.ENABLE_HTTP3 | default "false") }} +{{- $_ := set $config "enable_proxy_protocol" ($globals.Env.ENABLE_PROXY_PROTOCOL | default "false" | parseBool) }} {{- $_ := set $config "enable_http_on_missing_cert" ($globals.Env.ENABLE_HTTP_ON_MISSING_CERT | default "true") }} {{- $_ := set $config "https_method" ($globals.Env.HTTPS_METHOD | default "redirect") }} {{- $_ := set $config "non_get_redirect" ($globals.Env.NON_GET_REDIRECT | default "301") }} @@ -440,6 +441,19 @@ upstream {{ $vpath.upstream }} { {{- when .Enable "access_log /var/log/nginx/access.log vhost;" "" }} {{- end }} +map $proxy_add_x_forwarded_for $proxy_x_forwarded_for { + {{- if $globals.config.trust_downstream_proxy }} + {{- if $globals.config.enable_proxy_protocol }} + default $proxy_protocol_addr; + {{- else }} + default $proxy_add_x_forwarded_for; + {{- end }} + {{- else }} + default $remote_addr; + {{- end }} + '' $remote_addr; +} + # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { @@ -454,8 +468,20 @@ map $http_x_forwarded_host $proxy_x_forwarded_host { # If we receive X-Forwarded-Port, pass it through; otherwise, pass along the # server port the client connected to -map $http_x_forwarded_port $proxy_x_forwarded_port { - default {{ if $globals.config.trust_downstream_proxy }}$http_x_forwarded_port{{ else }}$server_port{{ end }}; +map $http_x_forwarded_port $_proxy_x_forwarded_port { + {{ if $globals.config.trust_downstream_proxy }} + {{ if $globals.config.enable_proxy_protocol }} + default $proxy_protocol_server_port; + {{- else }} + default $http_x_forwarded_port; + {{- end }} + {{- else }} + default $server_port; + {{- end }} +} + +map $_proxy_x_forwarded_port $proxy_x_forwarded_port { + default $_proxy_x_forwarded_port; '' $server_port; } @@ -559,7 +585,7 @@ proxy_set_header Host $host$host_port; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-For $proxy_x_forwarded_for; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; @@ -821,6 +847,7 @@ proxy_set_header Proxy ""; {{- $default_https_exists = or $default_https_exists (and $https $vhost.default) }} {{- $http3_enabled = or $http3_enabled $vhost.http3_enabled }} {{- end }} + {{- $proxy_protocol := when $globals.config.enable_proxy_protocol " proxy_protocol" "" }} {{- $fallback_http := not $default_http_exists }} {{- $fallback_https := not $default_https_exists }} {{- /* @@ -838,21 +865,21 @@ server { {{ template "access_log" (dict "Enable" $globals.config.enable_access_log) }} http2 on; {{- if $fallback_http }} - listen {{ $globals.config.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_http_port }} {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} + listen [::]:{{ $globals.config.external_http_port }} {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- end }} {{- if $fallback_https }} - listen {{ $globals.config.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_https_port }} ssl {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} + listen [::]:{{ $globals.config.external_https_port }} ssl {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- if $http3_enabled }} http3 on; - listen {{ $globals.config.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_https_port }} quic reuseport {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} + listen [::]:{{ $globals.config.external_https_port }} quic reuseport {{- $proxy_protocol }}; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- end }} ssl_session_cache shared:SSL:50m; @@ -892,7 +919,8 @@ server { {{- end }} {{- range $hostname, $vhost := $globals.vhosts }} - {{- $default_server := when $vhost.default "default_server" "" }} + {{- $default_server := when $vhost.default " default_server" "" }} + {{- $proxy_protocol := when $globals.config.enable_proxy_protocol " proxy_protocol" "" }} {{- range $path, $vpath := $vhost.paths }} # {{ $hostname }}{{ $path }} @@ -906,9 +934,9 @@ server { server_tokens {{ $vhost.server_tokens }}; {{- end }} {{ template "access_log" (dict "Enable" $globals.config.enable_access_log) }} - listen {{ $globals.config.external_http_port }} {{ $default_server }}; + listen {{ $globals.config.external_http_port }} {{- $default_server }} {{- $proxy_protocol }}; {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_http_port }} {{ $default_server }}; + listen [::]:{{ $globals.config.external_http_port }} {{- $default_server }} {{- $proxy_protocol }}; {{- end }} {{- if (or $vhost.acme_http_challenge_legacy $vhost.acme_http_challenge_enabled) }} @@ -967,9 +995,9 @@ server { http2 on; {{- end }} {{- if or (eq $vhost.https_method "nohttps") (eq $vhost.https_method "noredirect") }} - listen {{ $globals.config.external_http_port }} {{ $default_server }}; + listen {{ $globals.config.external_http_port }} {{- $default_server }} {{- $proxy_protocol }}; {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_http_port }} {{ $default_server }}; + listen [::]:{{ $globals.config.external_http_port }} {{- $default_server }} {{- $proxy_protocol }}; {{- end }} {{- if (and (eq $vhost.https_method "noredirect") $vhost.acme_http_challenge_enabled) }} @@ -984,17 +1012,17 @@ server { {{- end }} {{- end }} {{- if ne $vhost.https_method "nohttps" }} - listen {{ $globals.config.external_https_port }} ssl {{ $default_server }}; + listen {{ $globals.config.external_https_port }} ssl {{- $default_server }} {{- $proxy_protocol }}; {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_https_port }} ssl {{ $default_server }}; + listen [::]:{{ $globals.config.external_https_port }} ssl {{- $default_server }} {{- $proxy_protocol }}; {{- end }} {{- if $vhost.http3_enabled }} http3 on; add_header alt-svc 'h3=":{{ $globals.config.external_https_port }}"; ma=86400;'; - listen {{ $globals.config.external_https_port }} quic {{ $default_server }}; + listen {{ $globals.config.external_https_port }} quic {{- $default_server }} {{- $proxy_protocol }}; {{- if $globals.config.enable_ipv6 }} - listen [::]:{{ $globals.config.external_https_port }} quic {{ $default_server }}; + listen [::]:{{ $globals.config.external_https_port }} quic {{- $default_server }} {{- $proxy_protocol }}; {{- end }} {{- end }} diff --git a/test/test_proxy_protocol/test_proxy-protocol-global-disabled.py b/test/test_proxy_protocol/test_proxy-protocol-global-disabled.py new file mode 100644 index 0000000..b4ea76f --- /dev/null +++ b/test/test_proxy_protocol/test_proxy-protocol-global-disabled.py @@ -0,0 +1,58 @@ +import socket + + +def test_proxy_protocol_global_disabled_X_Forwarded_For_is_generated( + docker_compose, nginxproxy +): + r = nginxproxy.get("http://proxy-protocol-global-disabled.nginx-proxy.tld/headers") + assert r.status_code == 200 + assert "X-Forwarded-For:" in r.text + + +def test_proxy_protocol_global_disabled_X_Forwarded_For_is_passed_on( + docker_compose, nginxproxy +): + r = nginxproxy.get( + "http://proxy-protocol-global-disabled.nginx-proxy.tld/headers", + headers={"X-Forwarded-For": "1.2.3.4"}, + ) + assert r.status_code == 200 + assert "X-Forwarded-For: 1.2.3.4, " in r.text + + +def test_proxy_protocol_global_disabled_X_Forwarded_Port_is_generated( + docker_compose, nginxproxy +): + r = nginxproxy.get("http://proxy-protocol-global-disabled.nginx-proxy.tld/headers") + assert r.status_code == 200 + assert "X-Forwarded-Port: 80\n" in r.text + + +def test_proxy_protocol_global_disabled_X_Forwarded_Port_is_passed_on( + docker_compose, nginxproxy +): + r = nginxproxy.get( + "http://proxy-protocol-global-disabled.nginx-proxy.tld/headers", + headers={"X-Forwarded-Port": "1234"}, + ) + assert r.status_code == 200 + assert "X-Forwarded-Port: 1234\n" in r.text + + +def test_proxy_protocol_global_disabled_proto_request_fails(docker_compose, nginxproxy): + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect((nginxproxy.get_ip(), 80)) + + # 1.2.3.4 is the client IP + # 4.3.2.1 is the proxy server IP + # 8080 is the client port + # 9090 is the proxy server port + client.send(f"PROXY TCP4 1.2.3.4 4.3.2.1 8080 9090\r\n".encode("utf-8")) + client.send( + "GET /headers HTTP/1.1\r\nHost: proxy-protocol-global-enabled.nginx-proxy.tld\r\n\r\n".encode( + "utf-8" + ) + ) + + response = client.recv(4096).decode("utf-8") + assert "HTTP/1.1 400 Bad Request" in response diff --git a/test/test_proxy_protocol/test_proxy-protocol-global-disabled.yml b/test/test_proxy_protocol/test_proxy-protocol-global-disabled.yml new file mode 100644 index 0000000..593e21f --- /dev/null +++ b/test/test_proxy_protocol/test_proxy-protocol-global-disabled.yml @@ -0,0 +1,12 @@ +services: +# nginx-proxy: +# environment: +# ENABLE_PROXY_PROTOCOL: "false" #Disabled by default + + proxy-protocol-global-disabled: + image: web + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST: proxy-protocol-global-disabled.nginx-proxy.tld diff --git a/test/test_proxy_protocol/test_proxy-protocol-global-enabled.py b/test/test_proxy_protocol/test_proxy-protocol-global-enabled.py new file mode 100644 index 0000000..48a8f7e --- /dev/null +++ b/test/test_proxy_protocol/test_proxy-protocol-global-enabled.py @@ -0,0 +1,31 @@ +import socket + + +def test_proxy_protocol_global_enabled_normal_request_fails(docker_compose, nginxproxy): + try: + r = nginxproxy.get( + "http://proxy-protocol-global-enabled.nginx-proxy.tld/headers" + ) + assert False + except Exception as e: + assert "Remote end closed connection without response" in str(e) + + +def test_proxy_protocol_global_enabled_proto_request_works(docker_compose, nginxproxy): + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect((nginxproxy.get_ip(), 80)) + + # 1.2.3.4 is the client IP + # 4.3.2.1 is the proxy server IP + # 8080 is the client port + # 9090 is the proxy server port + client.send(f"PROXY TCP4 1.2.3.4 4.3.2.1 8080 9090\r\n".encode("utf-8")) + client.send( + "GET /headers HTTP/1.1\r\nHost: proxy-protocol-global-enabled.nginx-proxy.tld\r\n\r\n".encode( + "utf-8" + ) + ) + + response = client.recv(4096).decode("utf-8") + assert "X-Forwarded-For: 1.2.3.4" in response + assert "X-Forwarded-Port: 9090" in response diff --git a/test/test_proxy_protocol/test_proxy-protocol-global-enabled.yml b/test/test_proxy_protocol/test_proxy-protocol-global-enabled.yml new file mode 100644 index 0000000..04f7a37 --- /dev/null +++ b/test/test_proxy_protocol/test_proxy-protocol-global-enabled.yml @@ -0,0 +1,12 @@ +services: + nginx-proxy: + environment: + ENABLE_PROXY_PROTOCOL: "true" + + proxy-protocol-global-enabled: + image: web + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST: proxy-protocol-global-enabled.nginx-proxy.tld