1
0
mirror of https://github.com/nginx-proxy/nginx-proxy synced 2024-11-08 07:49:22 +01:00

Merge pull request #2278 from nginx-proxy/http3

feat: experimental HTTP/3 support + optional HTTP/2 disabling
This commit is contained in:
Nicolas Duchon 2023-12-08 01:40:36 +01:00 committed by GitHub
commit d05175d1d6
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 269 additions and 8 deletions

@ -388,6 +388,35 @@ If the default certificate is also missing, nginx-proxy will configure nginx to
>
> Error code: `SSL_ERROR_INTERNAL_ERROR_ALERT` "TLS error".
### HTTP/2 support
HTTP/2 is enabled by default and can be disabled if necessary either per-proxied container or globally:
To disable HTTP/2 for a single proxied container, set the `com.github.nginx-proxy.nginx-proxy.http2.enable` label to `false` on this container.
To disable HTTP/2 globally set the environment variable `ENABLE_HTTP2` to `false` on the nginx-proxy container.
More reading on the potential TCP head-of-line blocking issue with HTTP/2: [HTTP/2 Issues](https://www.twilio.com/blog/2017/10/http2-issues.html), [Comparing HTTP/3 vs HTTP/2](https://blog.cloudflare.com/http-3-vs-http-2/)
### HTTP/3 support
> **Warning**
> HTTP/3 support [is still considered experimental in nginx](https://www.nginx.com/blog/binary-packages-for-preview-nginx-quic-http3-implementation/) and as such is considered experimental in nginx-proxy too and is disabled by default. [Feedbacks for the HTTP/3 support are welcome in #2271.](https://github.com/nginx-proxy/nginx-proxy/discussions/2271)
HTTP/3 use the QUIC protocol over UDP (unlike HTTP/1.1 and HTTP/2 which work over TCP), so if you want to use HTTP/3 you'll have to explicitely publish the 443/udp port of the proxy in addition to the 443/tcp port:
```console
docker run -d -p 80:80 -p 443:443/tcp -p 443:443/udp \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
nginxproxy/nginx-proxy
```
HTTP/3 can be enabled either per-proxied container or globally:
To enable HTTP/3 for a single proxied container, set the `com.github.nginx-proxy.nginx-proxy.http3.enable` label to `true` on this container.
To enable HTTP/3 globally set the environment variable `ENABLE_HTTP3` to `true` on the nginx-proxy container.
### Basic Authentication Support
In order to be able to secure your virtual host, you have to create a file named as its equivalent VIRTUAL_HOST variable on directory

@ -277,8 +277,8 @@ map $http_x_forwarded_proto $proxy_x_forwarded_proto {
}
map $http_x_forwarded_host $proxy_x_forwarded_host {
default {{ if $globals.trust_downstream_proxy }}$http_x_forwarded_host{{ else }}$http_host{{ end }};
'' $http_host;
default {{ if $globals.trust_downstream_proxy }}$http_x_forwarded_host{{ else }}$host{{ end }};
'' $host;
}
# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
@ -350,7 +350,7 @@ include /etc/nginx/proxy.conf;
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
@ -384,7 +384,15 @@ proxy_set_header Proxy "";
{{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }}
{{- $default := eq $globals.Env.DEFAULT_HOST $vhost }}
{{- $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }}
{{- $_ := set $globals.vhosts $vhost (dict "cert" $cert "cert_ok" $cert_ok "containers" $containers "default" $default "https_method" $https_method) }}
{{- $http3 := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}}
{{- $_ := set $globals.vhosts $vhost (dict
"cert" $cert
"cert_ok" $cert_ok
"containers" $containers
"default" $default
"https_method" $https_method
"http3" $http3
) }}
{{- end }}
{{- /*
@ -406,6 +414,7 @@ proxy_set_header Proxy "";
{{- $https_exists := false }}
{{- $default_http_exists := false }}
{{- $default_https_exists := false }}
{{- $http3 := false }}
{{- range $vhost := $globals.vhosts }}
{{- $http := or (ne $vhost.https_method "nohttp") (not $vhost.cert_ok) }}
{{- $https := ne $vhost.https_method "nohttps" }}
@ -413,6 +422,7 @@ proxy_set_header Proxy "";
{{- $https_exists = or $https_exists $https }}
{{- $default_http_exists = or $default_http_exists (and $http $vhost.default) }}
{{- $default_https_exists = or $default_https_exists (and $https $vhost.default) }}
{{- $http3 = or $http3 $vhost.http3 }}
{{- end }}
{{- $fallback_http := and $http_exists (not $default_http_exists) }}
{{- $fallback_https := and $https_exists (not $default_https_exists) }}
@ -429,6 +439,7 @@ proxy_set_header Proxy "";
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
server_tokens off;
{{ $globals.access_log }}
http2 on;
{{- if $fallback_http }}
listen {{ $globals.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}}
@ -441,10 +452,16 @@ server {
{{- if $globals.enable_ipv6 }}
listen [::]:{{ $globals.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}}
{{- end }}
{{- if $http3 }}
http3 on;
listen {{ $globals.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}}
{{- if $globals.enable_ipv6 }}
listen [::]:{{ $globals.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}}
{{- end }}
{{- end }}
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
{{- end }}
{{ $globals.access_log }}
{{- if $globals.default_cert_ok }}
ssl_certificate /etc/nginx/certs/default.crt;
ssl_certificate_key /etc/nginx/certs/default.key;
@ -471,6 +488,8 @@ server {
{{- $containers := $vhost.containers }}
{{- $default_server := when $vhost.default "default_server" "" }}
{{- $https_method := $vhost.https_method }}
{{- $http2 := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}}
{{- $http3 := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}}
{{- $is_regexp := hasPrefix "~" $host }}
{{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $host) $host }}
@ -518,11 +537,11 @@ server {
{{- if $server_tokens }}
server_tokens {{ $server_tokens }};
{{- end }}
{{ $globals.access_log }}
listen {{ $globals.external_http_port }} {{ $default_server }};
{{- if $globals.enable_ipv6 }}
listen [::]:{{ $globals.external_http_port }} {{ $default_server }};
{{- end }}
{{ $globals.access_log }}
# Do not HTTPS redirect Let's Encrypt ACME challenge
location ^~ /.well-known/acme-challenge/ {
@ -549,8 +568,10 @@ server {
{{- if $server_tokens }}
server_tokens {{ $server_tokens }};
{{- end }}
http2 on;
{{ $globals.access_log }}
{{- if $http2 }}
http2 on;
{{- end }}
{{- if or (eq $https_method "nohttps") (not $cert_ok) (eq $https_method "noredirect") }}
listen {{ $globals.external_http_port }} {{ $default_server }};
{{- if $globals.enable_ipv6 }}
@ -563,6 +584,15 @@ server {
listen [::]:{{ $globals.external_https_port }} ssl {{ $default_server }};
{{- end }}
{{- if $http3 }}
http3 on;
add_header alt-svc 'h3=":{{ $globals.external_https_port }}"; ma=86400;';
listen {{ $globals.external_https_port }} quic {{ $default_server }};
{{- if $globals.enable_ipv6 }}
listen [::]:{{ $globals.external_https_port }} quic {{ $default_server }};
{{- end }}
{{- end }}
{{- if $cert_ok }}
{{- template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}
@ -645,7 +675,16 @@ server {
{{- $upstream = printf "%s-%s" $upstream $sum }}
{{- $dest = (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }}
{{- end }}
{{- template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag "Containers" $containers) }}
{{- template "location" (dict
"Path" $path
"Proto" $proto
"Upstream" $upstream
"Host" $host
"VhostRoot" $vhost_root
"Dest" $dest
"NetworkTag" $network_tag
"Containers" $containers
) }}
{{- end }}
{{- if and (not (contains $paths "/")) (ne $globals.default_root_response "none")}}
location / {

@ -0,0 +1,8 @@
import pytest
import re
def test_http2_global_disabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http2-global-disabled.nginx-proxy.tld")
assert r.status_code == 200
assert not re.search(r"(?s)http2-global-disabled\.nginx-proxy\.tld.*http2 on", conf)

@ -0,0 +1,15 @@
services:
http2-global-disabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http2-global-disabled.nginx-proxy.tld
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
ENABLE_HTTP2: "false"

@ -0,0 +1,19 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
def test_http3_global_disabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-global-disabled.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: http3-global-disabled.nginx-proxy.tld" in r.text
assert not "alt-svc" in r.headers
def test_http3_global_disabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http3-global-disabled.nginx-proxy.tld")
assert r.status_code == 200
assert not re.search(r"(?s)listen 443 quic", conf)
assert not re.search(r"(?s)http3 on", conf)
assert not re.search(r"(?s)add_header alt-svc \'h3=\":443\"; ma=86400;\'", conf)

@ -0,0 +1,15 @@
services:
http3-global-disabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http3-global-disabled.nginx-proxy.tld
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
#environment:
#ENABLE_HTTP3: "false" #Disabled by default

@ -0,0 +1,21 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
def test_http3_global_enabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-global-enabled.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: http3-global-enabled.nginx-proxy.tld" in r.text
assert "alt-svc" in r.headers
assert r.headers["alt-svc"] == 'h3=":443"; ma=86400;'
def test_http3_global_enabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http3-global-enabled.nginx-proxy.tld")
assert r.status_code == 200
assert re.search(r"listen 443 quic reuseport\;", conf)
assert re.search(r"(?s)http3-global-enabled\.nginx-proxy\.tld;.*listen 443 quic", conf)
assert re.search(r"(?s)http3-global-enabled\.nginx-proxy\.tld;.*http3 on\;", conf)
assert re.search(r"(?s)http3-global-enabled\.nginx-proxy\.tld;.*add_header alt-svc \'h3=\":443\"; ma=86400;\'", conf)

@ -0,0 +1,15 @@
services:
http3-global-enabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http3-global-enabled.nginx-proxy.tld
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
ENABLE_HTTP3: "true"

@ -0,0 +1,49 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
def test_http3_vhost_enabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-vhost-enabled.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: http3-vhost-enabled.nginx-proxy.tld" in r.text
assert "alt-svc" in r.headers
assert r.headers["alt-svc"] == 'h3=":443"; ma=86400;'
def test_http3_vhost_enabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http3-vhost-enabled.nginx-proxy.tld")
assert r.status_code == 200
assert re.search(r"listen 443 quic reuseport\;", conf)
assert re.search(r"(?s)http3-vhost-enabled\.nginx-proxy\.tld;.*listen 443 quic", conf)
assert re.search(r"(?s)http3-vhost-enabled\.nginx-proxy\.tld;.*http3 on\;", conf)
assert re.search(r"(?s)http3-vhost-enabled\.nginx-proxy\.tld;.*add_header alt-svc \'h3=\":443\"; ma=86400;\'", conf)
def test_http3_vhost_disabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-vhost-disabled.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: http3-vhost-disabled.nginx-proxy.tld" in r.text
assert not "alt-svc" in r.headers
def test_http3_vhost_disabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http3-vhost-disabled.nginx-proxy.tld")
assert r.status_code == 200
assert not re.search(r"(?s)http3-vhost-disabled\.nginx-proxy\.tld.*listen 443 quic.*\# http3-vhost-enabled\.nginx-proxy\.tld", conf)
assert not re.search(r"(?s)http3-vhost-disabled\.nginx-proxy\.tld.*http3 on.*\# http3-vhost-enabled\.nginx-proxy\.tld", conf)
assert not re.search(r"(?s)http3-vhost-disabled\.nginx-proxy\.tld;.*add_header alt-svc \'h3=\":443\"; ma=86400;\'.*\# http3-vhost-enabled\.nginx-proxy\.tld", conf)
def test_http3_vhost_disabledbydefault_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-vhost-default-disabled.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: http3-vhost-default-disabled.nginx-proxy.tld" in r.text
assert not "alt-svc" in r.headers
def test_http3_vhost_disabledbydefault_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http3-vhost-default-disabled.nginx-proxy.tld")
assert r.status_code == 200
assert not re.search(r"(?s)http3-vhost-default-disabled\.nginx-proxy\.tld.*listen 443 quic.*\# http3-vhost-disabled\.nginx-proxy\.tld", conf)
assert not re.search(r"(?s)http3-vhost-default-disabled\.nginx-proxy\.tld.*http3 on.*\# http3-vhost-disabled\.nginx-proxy\.tld", conf)
assert not re.search(r"(?s)http3-vhost-default-disabled\.nginx-proxy\.tld;.*add_header alt-svc \'h3=\":443\"; ma=86400;\'.*\# http3-vhost-disabled\.nginx-proxy\.tld", conf)

@ -0,0 +1,33 @@
services:
http3-vhost-enabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http3-vhost-enabled.nginx-proxy.tld
labels:
com.github.nginx-proxy.nginx-proxy.http3.enable: "true"
http3-vhost-disabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http3-vhost-disabled.nginx-proxy.tld
labels:
com.github.nginx-proxy.nginx-proxy.http3.enable: "false"
http3-vhost-default-disabled:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: http3-vhost-default-disabled.nginx-proxy.tld
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

@ -31,3 +31,11 @@ def test_web4_HSTS_off_noredirect(docker_compose, nginxproxy):
r = nginxproxy.get("https://web4.nginx-proxy.tld/port", allow_redirects=False)
assert "answer from port 81\n" in r.text
assert "Strict-Transport-Security" not in r.headers
def test_http3_vhost_enabled_HSTS_default(docker_compose, nginxproxy):
r = nginxproxy.get("https://http3-vhost-enabled.nginx-proxy.tld/port", allow_redirects=False)
assert "answer from port 81\n" in r.text
assert "Strict-Transport-Security" in r.headers
assert "max-age=31536000" == r.headers["Strict-Transport-Security"]
assert "alt-svc" in r.headers
assert r.headers["alt-svc"] == 'h3=":443"; ma=86400;'

@ -34,6 +34,16 @@ web4:
HSTS: "off"
HTTPS_METHOD: "noredirect"
web5:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: http3-vhost-enabled.nginx-proxy.tld
labels:
com.github.nginx-proxy.nginx-proxy.http3.enable: "true"
sut:
image: nginxproxy/nginx-proxy:test
volumes: