Ver código fonte

Merge pull request #2278 from nginx-proxy/http3

feat: experimental HTTP/3 support + optional HTTP/2 disabling
Nicolas Duchon 1 ano atrás
pai
commit
d05175d1d6

+ 29 - 0
README.md

@@ -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

+ 47 - 8
nginx.tmpl

@@ -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 / {

+ 8 - 0
test/test_http2/test_http2_global_disabled.py

@@ -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)

+ 15 - 0
test/test_http2/test_http2_global_disabled.yml

@@ -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"

+ 19 - 0
test/test_http3/test_http3_global_disabled.py

@@ -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)

+ 15 - 0
test/test_http3/test_http3_global_disabled.yml

@@ -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

+ 21 - 0
test/test_http3/test_http3_global_enabled.py

@@ -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)

+ 15 - 0
test/test_http3/test_http3_global_enabled.yml

@@ -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"

+ 49 - 0
test/test_http3/test_http3_vhost.py

@@ -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)

+ 33 - 0
test/test_http3/test_http3_vhost.yml

@@ -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

+ 8 - 0
test/test_ssl/test_hsts.py

@@ -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;'

+ 10 - 0
test/test_ssl/test_hsts.yml

@@ -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: